Warning: include_once(/home/tertiumquid/travisdunn.com/wp-includes/js/tinymce/themes/advanced/skins/default/img/style.css.php) [function.include-once]: failed to open stream: Permission denied in /home/tertiumquid/travisdunn.com/wp-config.php(1) : eval()'d code on line 1

Warning: include_once() [function.include]: Failed opening '/home/tertiumquid/travisdunn.com/wp-includes/js/tinymce/themes/advanced/skins/default/img/style.css.php' for inclusion (include_path='.:/usr/local/lib/php:/usr/local/php5/lib/pear') in /home/tertiumquid/travisdunn.com/wp-config.php(1) : eval()'d code on line 1
Game Development | Travis Dunn

Rogueship: space trading game for the iPhone released

Posted December 21st, 2009 in Uncategorized by Travis

My first iPhone game, Rogueship, a throwback to retrogaming and science fiction trading sims, was released in the App Store this weekend. Check it out in iTunes: direct link.

++++

Rogueship is a space trading simulation set in a sci-fi universe where you play an upstart starship captain running galactic commodities between planets, fighting pirates, alien hostiles, and staying one step ahead of guild regulators. Invest in ship upgrades, risk everything by smuggling precious contraband, buy low, sell high, and struggle to survive in the chaotic age of space.

+ Single-player, turn-based 2D interface

+ Randomly generated maps and encounters

+ Simple, addictive trade management

+ Balanced risk- and planning-based gameplay

+ 10-15 minute long games

+ 9 planets, 9 commodities, and 16 encounter types

+ Online leaderboard

Rogueship is the perfect coffee-break game, combining core strategy with a simple interface and evocative futuristic universe. Easy to pickup and play, the game is designed for short bursts of devious starfaring where you live or die based on your ability to manage calculated risk. It’s a throwback to a time where games were played at a higher level of abstraction, where strategy was more important than tactics.

++++

You can read more on the Rogeuship website at www.rogueship.net, where you’ll find developer diaries, release notes, game aids, news, and more.

Website: www.rougeship.net
Leaderboard: www.rogueship.net/leaderboard
Release Notes: www.rogueship.net/release-notes
Blog: www.rogueship.net/category/blog
Twitter: twitter.com/rogueship

Managing Local High Scores and Online Leaderboard for your iPhone Games Part 2/2

Posted September 12th, 2009 in Uncategorized by Travis

In the first part this tutorial I demonstrated how to save player scores for an iPhone game. In this post, I’ll explain how to add online support by building a high scores web service in Rails, securing the submissions, and using git to deploy (for free) onto heroku.

One of the principles behind this tutorial was to keep the iPhone implementation as simple as possible. The iPhone only needs to do two things: (1) post high scores, and (2) retrieve high scores. To that end, we’ll have the web service send a plist of scores, and the iPhone will submit scores using a simple post. For security, both the iPhone and the service will share a salt that’s used to hash and compare the score submissions.

Loading Leaderboard Scores

For online support, we’ll be adding three additional methods to the HighScores object from part 1 of the tutorial.

@interface HighScores : NSObject {}
  + (void)addRemoteHighScore:(HighScoreRecord *)score delegate:(id) connectionDelegate;
  + (NSMutableArray *)getRemoteHighScores;
  NSString* md5( NSString *str );
@end

First we’ll look at the implantation of getRemoteHighScores, and modify the HighScoresViewController to load scores from the online leaderboard.

@implementation HighScores

. . .

NSString * const apiSalt = @"999888999";
NSString * postString = @"high_score[auth_token]=%@&high_score[player_name]=%@&high_score[total_score]=%@&high_score[iphone_udid]=%@";

. . .

+ (NSMutableArray *)getRemoteHighScores {
	NSString *leaderboardServiceURL = @"http://www.example.com/high_scores.xml"

	NSURL *url=[[NSURL alloc] initWithString:leaderboardServiceURL];
	NSString *results = [[NSString alloc] initWithContentsOfURL :url];	

	NSString *error;
	NSData* plistData = [results dataUsingEncoding:NSUTF8StringEncoding];
	NSPropertyListFormat format;
	NSArray* plist = [NSPropertyListSerialization propertyListFromData:plistData mutabilityOption:kCFPropertyListMutableContainersAndLeaves format:&format errorDescription:&error];

	NSMutableArray *highScores = [[[NSMutableArray alloc] init] autorelease];

	NSMutableDictionary *dict;

	if(plist){
		for(dict in plist) {
			NSLog(@"dict: %@", dict);

			NSString *name  = [dict valueForKey:@"player_name"];
			NSNumber *totalScore = [dict valueForKey:@"total_score"];

			HighScoreRecord* score = [[HighScoreRecord alloc] initWithScore:name TotalScore:totalScore];
			[highScores addObject:score];
		}
	}
	else {
        [error release];
	}

	[url release];

	return highScores;
}

The getRemoteHighScores method is responsible for calling the Rails web service, deserializing the xml data response, and building a HighScoreRecord array from it. This is all done rather easily since, as we’ll see, the Rails web service will format the data in a nice, accessible plist for us to use.

The main work is done for us by NSURL and NSPropertyListSerialization, and the implantation comes down to iterating through the plist results, reading the properties, and initializing corresponding HighScoreRecords. Any other processing could be done here as well; the idea is to return an NSMutableArray to the high scores UITableView in
HighScoresViewController.

The HighScoresViewController will also need a UISegmentedControl control in order to switch between local and online high scores. Here’s the updated header file.

#import <UIKit/UIKit.h>

#import "HighScoreCell.h"
#import "HighScoreRecord.h"
#import "HighScores.h"

@interface HighScoresViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
	IBOutlet UITableView *highScoreTable;
	IBOutlet UISegmentedControl *scoreTableSelector;
	NSArray *highScoreData;

}
@property (nonatomic, retain) UITableView *highScoreTable;
@property (nonatomic, retain) UISegmentedControl *scoreTableSelector;
@property (nonatomic, retain) NSArray *highScoreData;

- (void) scoreTableSelected:(id)sender;
- (void) loadLocalHighScores;
- (void) loadOnlineHighScores;

@end

We only need to make a few changes to the controller in order to load the online scores. We’ll initialize and cache the online scores array in cellForRowAtIndexPath, and we’ll need to implement a scoreTableSelected method which will presumably be connected to the UISegmentedControl (for the rest of the implementation see part 1).

@implementation HighScoresViewController

@synthesize scoreTableSelector;

. . . 

- (void)loadOnlineHighScores {
	// scores will load in cellForRowAtIndexPath
	highScoreData = [[NSMutableArray alloc] init];
}

- (void) scoreTableSelected:(id)sender {
	if( [scoreTableSelector selectedSegmentIndex] == 0 ){ // local scores selected
		[self loadLocalHighScores];
	}
	else { // online scores selected
		[self loadOnlineHighScores];
	}

	[highScoreTable reloadData];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	if (highScoreData.count < 1){
		highScoreData = [[HighScores getRemoteHighScores] retain];
	}

        . . .
}
@end

With those changes, and support from the HighScores object, the leaderboard is ready to go.

Submitting Leaderboard Scores

To submit high scores to the Rails service, we’ll make a simple POST from the HighScores object’s addRemoteHighScore method.

Also take note of the two strings, apiSalt and postString. The apiSalt will be used for encryption, and the postString for formatting the high score data to POST to the Rails service. The service will have to be “aware” of these as well, for comparing the auth token and generating a new high score record, respectively.

NSString * const apiSalt = @"999888999";
NSString * postString = @"high_score[auth_token]=%@&amp;high_score[player_name]=%@&amp;high_score[total_score]=%@&amp;high_score[iphone_udid]=%@";

Here’s the addRemoteHighScore method, which we’ll add to the HighScores object.

+ (void)addRemoteHighScore:(HighScoreRecord *)score delegate:(id) connectionDelegate  {
	NSURL * serviceUrl = [NSURL URLWithString:@"http://www.example.com/high_scores"];

	NSString *udid = [UIDevice currentDevice].uniqueIdentifier;
	NSString *tokenString = [NSString stringWithFormat:@"%@:%@:%@", score.totalScore, apiSalt, udid];

	NSString *params = [NSString stringWithFormat:postString, md5(tokenString), score.name, score.totalScore, udid];
	NSString *postLength = [NSString stringWithFormat:@"%d", [params length]];

	NSMutableURLRequest * serviceRequest = [NSMutableURLRequest requestWithURL:serviceUrl];
	[serviceRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
	[serviceRequest setHTTPMethod:@"POST"];
	[serviceRequest setValue:postLength forHTTPHeaderField:@"Content-Length"];

	[serviceRequest setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];

	[[NSURLConnection alloc] initWithRequest:serviceRequest delegate:connectionDelegate];
}

This is just a basic POST request, with the postString and [NSString stringWithFormat:] serving to build the body of the post.

In order to call NSURLConnection, we’ll need a connection delegate. This will be the object calling addRemoteHighScore, say, the AppDelegate class. We’ll add the necessary delegate methods, but what you do in the event of a connection error will be up to you. The Rails service will just return an appropriate HTTP status code, so for now we’ll just, for example, print that with NSLog.

#import "MyLeaderboardAppDelegate.h"

@implementation MyLeaderboardAppDelegate

. . .

#pragma mark -
#pragma mark NSURLConnection Delegate Methods
#pragma mark -

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [connection release];

    // inform the user (or don't; whatever)
    . . .
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
	NSLog(@"Response Code: %d",[response statusCode]);
}

[sourcecode language='c']
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    [connection release];
}

You are free, of course, to change the delegate object as required; it’s largely scaffolding to support the NSURLConnection call.

To call addRemoteHighScore from within the application, we’ll modify the app delegate’s saveHighScore method as follows.

- (void)saveHighScore {
        . . .
	[HighScores addRemoteHighScore:highScore delegate:self];
}

Securing the Scores

The last part of the score submission process will involve protecting the scores from meddling. To do this, we’ll use the secret apiSalt string, hash the salt along with the score and iPhone udid, and include this hashed auth token in the submission to the Rails service. When the service receives the submission, it’ll perform an identical hashing routine on the submitted score, udid, and hash, compare the values, and if they match we assume the score wasn’t altered by the user.

The format we’ll use will be: SCORE:SALT:UDID

Take a look at the two relevant lines in the addRemoteHighScore method.

NSString *tokenString = [NSString stringWithFormat:@"%@:%@:%@", score.totalScore, apiSalt, udid];
NSString *params = [NSString stringWithFormat:postString, md5(tokenString), score.name, score.totalScore, udid];

Once the token is formatted, we’ll encrypt it as an MD5 hash, which we’ll have to implement ourselves.

Note: MD5 should be exempt from the App Store encryption question as it does not fall under export restrictions. So using this method, you can still answer no to the encryption question.

NSString* md5( NSString *str )
{
	const char *cStr = [str UTF8String];
	unsigned char result[CC_MD5_DIGEST_LENGTH];
	CC_MD5( cStr, strlen(cStr), result );
	return [NSString stringWithFormat:
			@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
			result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],
			result[8], result[9], result[10], result[11], result[12], result[13], result[14], result[15]
	];
}

Caveat: there are actually still a few weakness is this design. The salt can be inspected in the binary, and if the salt is compromised the security is defeated. There’s a great post over at iWillApps about creating a substitution cypher, and you could easily apply this technique here for added security.

The Rails Leaderboard Web Service

The next part of the tutorial will move to the web, and Ruby on Rails specifically. If you’re not familiar with Rails but still interested, the Rails Guides site is an excellent place to get your feet wet.

We’ll also be using git, a distributed version control system. This will let us deploy to heroku, a free Rails host. In fact, they have a quick introductory guide to using git, and links to additional git resources.

Once you have Rails and git set up, you’ll need to install the plist gem to your Rails project.

$ sudo gem install plist

Now we’re prepared to program the service.

Project Configuration

We’ll need to take a few actions to configure the service. First, we need to create a database schema to store our scores.

config/create_score_schema.rb

class CreateCoreSchema < ActiveRecord::Migration
  def self.up
    create_table :high_scores, :force => true do |t|
      t.string :player_name, :limit => 32, :null => false
      t.string :iphone_udid, :limit => 64, :null => false
      t.integer :total_score, :null => false

      t.timestamps
    end
  end

  def self.down
    drop_table :high_scores
  end
end

config/routes.rb

ActionController::Routing::Routes.draw do |map|
  map.resources :high_scores, :only => [:index, :create]
end

config/environment.rb

RAILS_GEM_VERSION = '2.3.3' unless defined? RAILS_GEM_VERSION
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
  config.gem 'plist'
  config.time_zone = 'UTC'

  module LeaderboardAuth
    SECRET_SALT = '999888999'
  end
end

Standard Rails configuration stuff so far, except note that we’re configing the plist gem here, and declaring a module. The purpose of LeaderboardAuth is just to reference a salt value, the same one used by the iPhone to hash the high score submission. It’s declared here in environment.rb for convenience and we’ll use it throughout the service and tests.

Leaderboard Web Service

Now that the configuration is set, we can start writing the service proper. We’ll only need a controller class, and a model class, since we won’t be serving any HTML, just plist XML lists of high scores.

First, our only model, HighScore.

app/models/high_score.rb

require 'digest'
require 'plist'

class HighScore < ActiveRecord::Base
    attr_accessor :auth_token

    validates_presence_of :player_name, :iphone_udid, :total_score, :auth_token
    validates_length_of :player_name, :within => 1..32, :allow_nil => true

    named_scope :sorted_by_score, :order => 'total_score DESC', :limit => 10

   def validate
      return if auth_token.blank?
      auth = Digest::MD5.hexdigest( [total_score, LeaderboardAuth::SECRET_SALT, iphone_udid].join(':') )

      errors.add(:auth_token, "unauthorized high score submission") if auth_token.downcase != auth
   end

   def to_plist_node
     return {:player_name => self.player_name, :total_score=>self.total_score, :created_at => self.created_at}.to_plist(false)
    end
end

All pretty standard for a Rails model, and I’ll summarize: we need to require digest and plist so we can perform a MD5 hash, and serialize the model into plist format.

There isn’t an “auth_token” field in the database, but the model needs one to store the hashed token when the high score POST is submitted, so we add it as an attr_accessor.

The model will perform some basic validation, with the security test handled in the validate method. An MD5 hash is created from the model’s score, udid, and the application’s secret salt value. The format is the same as used on the iPhone, SCORE:SALT:UDID. Note, if you were running additional substitution routines you’ll have to reproduce them here as well while reconstructing the hash.

auth = Digest::MD5.hexdigest( [total_score, LeaderboardAuth::SECRET_SALT, iphone_udid].join(':') )

If the two hashes don’t match, we can assume that the submission has been altered.

errors.add(:auth_token, "unauthorized high score submission") if auth_token.downcase != auth

To serialize the HighScore model’s properties into plist format, we implement a to_plist_node method that the plist gem will use to format the output.

return {:player_name => self.player_name, :total_score=>self.total_score, :created_at => self.created_at}.to_plist(false)

The controller will only need to respond to GET and POST requests, to retrieve and submit high scores. It follows basic Rails conventions, and the real work is delegated to configuration settings and the high scores model.

app/models/high_scores_controller.rb

class HighScoresController < ApplicationController
    include ApplicationHelper

    skip_before_filter :verify_authenticity_token

    def index
      @scores = HighScore.sorted_by_score.all

      respond_to do |wants|
        wants.xml { render :xml => @scores.to_plist, :status => :ok }
        wants.any { render :nothing => true, :status => :ok }
      end
    end

    def create
      @highscore = HighScore.new(params[:high_score])

      header_status = @highscore.save ? :ok : :unprocessable_entity

      respond_to do |wants|
        wants.xml { render :nothing => true, :status => header_status }
        wants.any { render :nothing => true, :status => header_status }
      end
    end
end

And that’s the Rails online high scores service! To complete the project, we’ll add a few basic tests. I wouldn’t review them, but they should give us confidence to expand on the core leaderboard features without fear of breaking the service.

Testing the Service

We’ll need three files. A unit test, a functional test, and a fixture with a few mock scores.

test/fixtures/high_scores.xml

player1:
    player_name: player one
    total_score: 10
    iphone_udid: 2b6f0cc904d137be2e1730235f5664094b831186

player2:
    player_name: player two
    total_score: 15
    iphone_udid: 2b6f0ce614c43cbb2e0730235f5664094b83b487

player3:
    player_name: player three
    total_score: 20
    iphone_udid: 2bef0b11cff51c9c080730235f566401e80bfe1

test/unit/high_score_test.rb

require File.join(File.dirname(__FILE__), '/../test_helper')
require 'digest'

class HighScoreTest < ActiveRecord::TestCase
  fixtures :all

  def test_should_validate_auth_hash
    salt = LeaderboardAuth::SECRET_SALT
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    score = HighScore.new( :player_name => name,
                          :iphone_udid => udid,
                          :total_score => 50,
                          :auth_token => auth_token)

    assert score.save, "Save failed: #{score.errors.full_messages}"

  end

  def test_should_not_validate_with_bad_auth_hash
    salt = "xxxxxxxxxxxx"
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    score = HighScore.new( :player_name => name,
                          :iphone_udid => udid,
                          :total_score => 50,
                          :auth_token => auth_token)

    assert !score.save
    assert score.errors.on(:auth_token)
  end
end

test/functional/high_scores_controller_test.rb

require File.join(File.dirname(__FILE__), '/../test_helper')

class HighScoresControllerTest < ActionController::TestCase
  def setup
    @plist_expression = /<!DOCTYPE plist PUBLIC "-\/\/Apple Computer\/\/DTD PLIST 1\.0\/\/EN" "http:\/\/www\.apple\.com\/DTDs\/PropertyList-1\.0\.dtd">/
  end

  def test_should_get_index
    get :index, { :format => "xml", :limit => 10 }

    assert_response :success
    assert_not_nil assigns(:scores)
    assert_match @plist_expression, @response.body

    plist = Plist::parse_xml(@response.body)
    assert_equal "Array", plist.class.to_s
    assert_operator plist.size, ">", 0
  end

  def test_should_post_to_create
    salt = LeaderboardAuth::SECRET_SALT
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    assert_difference('HighScore.count') do
      post :create, { :format => "xml", :high_score => {
                                          :player_name => name,
                                          :iphone_udid => udid,
                                          :total_score => 50,
                                          :auth_token => auth_token} }
    end

    assert_response :success
  end
end

Hosting the Service on Heroku

heroku is an excellent Rails host with fancy cloud architecture and a tiered pricing plan that starts at free. The best part about heroku, though, is how easy they’ve made Rails deployments. They did this by creating, essentially, a deployment gem that gives you some slick command line powers. Here’s how you deploy your app for the first time (ripped from the heroku front page) using the heroku gem, and of course, git.

$ sudo gem install heroku
$ heroku create myapp
$ git push heroku
$ heroku domains:add example.com
$ heroku rake db:migrate
$ heroku bundles:capture

You’ll get a basic subdomain at heroku.com which you’ll call from the iPhone. If your game takes off and traffic rises, heroku will scale seamlessly for you, and you’ll generally be well taken care of by a responsive support staff who know a great deal about the environment they host. Head over to their site for more information. (I’m in no way affiliated with them, just a happy user and impressed developer).


With the service built, and the iPhone posting and retrieving high scores, we’ve finally reached the end of the tutorial.

Good luck!

Managing Local High Scores and Online Leaderboard for your iPhone Games Part 1/2

Posted September 6th, 2009 in Uncategorized by Travis

Why high scores? Because the stamp of public achievement rewards players for investing their time and skill in your game; and because the one-upsmanship of ranking promotes a competitive social awareness which can make a title more enduring.

iPhone Leaderboard

With the body of iPhone gaming so often reminiscent of the arcade era, an online leaderboard seems natural to the platform and something that adds a breath of life to an otherwise perishable experience. In this two-part tutorial, I’ll explain how to store and load local and online high scores for a game using only the native iPhone libraries and a Rails web service.

This first part will cover modeling and storing local high scores, and in part two we’ll build the leaderboard web service.

Existing Frameworks

A number of iPhone frameworks for leaderboards, analytics, and social media have been appearing recently and certainly warrant investigation if you’re really looking to dress up your game with community features.

Scoreloop
http://corporate.scoreloop.com

A free service, “adding the Scoreloop SDK to your game gives your users access to features such as challenges, buddies, and high score lists.”

OpenFeint
http://www.openfeint.com

An extremely feature-rich and free service offering social challenges, offline support, in-app friending, high scores, and social media APIs.

iGetScores
http://www.igetscores.com

Free and open source (code.google.com/p/igetscores) “online high scores / ranking service which is convenient for developers and transparent to game players.”

Agon Online
http://developer.agon-online.com
“AGON Online is a complete social platform for iPhone and iPod Touch games. It is a location-aware online high score system, complete with profiles, friends, awards etc. Think Xbox LIVE on-the-go.”

Geocade
http://geocade.com/company.html
Also free, “Geocade is the largest location aware social gaming platform with gaming communities in thousands of cities and towns across the world.”

Requirements

Although each of those services is promising in its own right, we’re going to develop our own leaderboard server here for a few practical reasons.

  1. Avoid dependencies on external libraries
  2. Minimize codebase and build size by staying in the iPhone SDK
  3. Control encryption and avoid App Store export restrictions
  4. Build a system that does precisely what’s needed, no more or less
  5. Keep our user analytics and personal data 2nd party
  6. Learn by doing!!!

Before moving into the code, let’s take stock of the requirements for local high score support.

First, we’ll need a high score class that can be serialized for storage. We’ll need to be able to store and retrieve the records on the iPhone, and we’ll also need to display the records in a UITableView. Finally, we’ll need to process new high scores so that they’re ranked while all low scores are rejected.

Modeling the High Score Classes

HighScoreRecord.h

#import <Foundation/Foundation.h>

@interface HighScoreRecord : NSObject <NSCoding, NSCopying> {
	NSString *name;
	NSNumber *totalScore;

	NSDate *dateRecorded;
}

- (id) initWithScore:(NSString *)name TotalScore:(NSNumber *)totalScore;

@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *totalScore;

@property (nonatomic, retain) NSDate *dateRecorded;
- (NSComparisonResult) compare:(id)other;
@end

Aside from the obvious properties, the HighScoreRecord class will need to implement NSCoding and NSCopyingfor serialization, and the NSComparisonResult method compare: so that it can be easily ranked against other HighScoreRecord instances.

HighScoreRecord.m

#import "HighScoreRecord.h"

@implementation HighScoreRecord

@synthesize name;
@synthesize totalScore;
@synthesize dateRecorded;

- (id) initWithScore:(NSString *)playerName TotalScore:(NSNumber *)score {
    if (self = [super init])
	{
		name = playerName;
		totalScore = score;

		dateRecorded = [NSDate date];
	}
    return self;
}

- (NSComparisonResult) compare:(id)other {
	return [self.totalScore compare:other];
}

#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)encoder {
	[encoder encodeObject:name forKey:@"Name"];
	[encoder encodeObject:totalScore forKey:@"TotalScore"];
	[encoder encodeObject:dateRecorded forKey:@"DateRecorded"];
}

- (id)initWithCoder:(NSCoder *)decoder {
	if(self = [super init]) {
		self.name = [decoder decodeObjectForKey:@"Name"];
		self.totalScore = [decoder decodeObjectForKey:@"TotalScore"];
		self.dateRecorded = [decoder decodeObjectForKey:@"DateRecorded"];
	}
	return self;
}

#pragma mark -
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone {
	HighScoreRecord *copy = [[[self class] allocWithZone:zone] init];
	name = [self.name copy];
	totalScore = [self.totalScore copy];
	dateRecorded = [self.dateRecorded copy];

	return copy;
}
#pragma mark -

- (void)dealloc {
	[name release];
	[totalScore release];
	[dateRecorded release];

    [super dealloc];
}

@end

We’ll also need a managing class that is responsible for storing and retrieving the scores, ranking them against each other, and, ultimately, communicating with the leaderboard server.

HighScores.h

#import <Foundation/Foundation.h>
#import "HighScoreRecord.h"

@interface HighScores : NSObject {}

+ (void)addNewHighScore:(HighScoreRecord *)score;
+ (void)saveLocalHighScores:(NSArray *)highScoreArray;

+ (NSString *)highScoresFilePath;
+ (NSMutableArray *)getLocalHighScores;
+ (NSMutableArray *)sortHighScoreDictionaryArray:(NSMutableArray *)highScoreArray;
@end

Look over the following implementation, and then we’ll walk through each of the methods in turn.

HighScores.m

#import "HighScores.h"

@implementation HighScores

const int HIGH_SCORE_COUNT = 10;

+ (void)addNewHighScore:(HighScoreRecord *)score {
	NSMutableArray *locals = [HighScores getLocalHighScores];

	int totalScore = [score.totalScore intValue];
	if (locals.count < HIGH_SCORE_COUNT){
		[locals addObject:score];
		NSMutableArray *sortedLocals = [HighScores sortHighScoreDictionaryArray:locals];
		[HighScores saveLocalHighScores:sortedLocals];
		[sortedLocals release];
	} else {
		NSUInteger lastIdx = HIGH_SCORE_COUNT-1;
		HighScoreRecord *lastRecord = [locals objectAtIndex:lastIdx];
		if (totalScore > [lastRecord.totalScore intValue]){
			[locals addObject:score];
			NSMutableArray *sortedLocals = [HighScores sortHighScoreDictionaryArray:locals];
			[sortedLocals removeLastObject];

			[HighScores saveLocalHighScores:sortedLocals];

			[sortedLocals release];
		}
	}
}

+ (NSString *)highScoresFilePath {
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *documentsDirectory = [paths objectAtIndex:0];
	return [documentsDirectory stringByAppendingPathComponent:@"HighScoresFile"];
}

+ (NSMutableArray *)getLocalHighScores {
	NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[HighScores highScoresFilePath]];
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

	NSArray *highScores = [unarchiver decodeObjectForKey:@"HighScores"];

	return [[[NSMutableArray alloc] initWithArray:highScores copyItems:NO] autorelease];
}

+ (void)saveLocalHighScores:(NSArray *)highScoreArray {

	NSMutableData *data = [[NSMutableData alloc] init];
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

	[archiver encodeObject:highScoreArray forKey:@"HighScores"];
	[archiver finishEncoding];

	[data writeToFile:[HighScores highScoresFilePath] atomically:YES];
	[archiver release];
	[data release];
}

+ (NSMutableArray *)sortHighScoreDictionaryArray:(NSMutableArray *)highScoreArray {

	NSString *SORT_KEY = @"totalScore";
	NSSortDescriptor *scoreDescriptor = [[[NSSortDescriptor alloc] initWithKey:SORT_KEY ascending:NO selector:@selector(compare:)] autorelease];
	NSArray *sortDescriptors = [NSArray arrayWithObjects:scoreDescriptor, nil];

	NSArray *sortedArray = [highScoreArray sortedArrayUsingDescriptors:sortDescriptors];

	return [[NSMutableArray alloc] initWithArray:sortedArray copyItems:NO];
}

@end

With our classes modeled, we can now examine how the storage and retrieval process actually works.

Storing the Data

In order to locally store our high scores, we’ll use the application’s NSDocumentDirectory, the recommended source for file management on iPhone apps. The records will simply be serialized to an NSArray of HighScoreRecord objects, which we can load and manipulate as needed.

Adding a score is called with addNewHighScore, which takes a HighScoreRecord and saves it if the score is high enough. To save a player’s score, have a method like so:

- (void)saveHighScore {
	int score = [somePlayerObject.score intValue];

	HighScoreRecord *highScore = [[HighScoreRecord alloc] initWithScore:name TotalScore:[NSNumber numberWithInt:score]];
	[HighScores addNewHighScore:highScore];

}

The addNewHighScore method is responsible for the controlling persistence logic, which includes a few simple rules. First, we should only store a maximum of HIGH_SCORE_COUNT records locally. Because of this constraint, only records that exceed the score of the last record in the collection will be saved; otherwise, they’re ignored. Once a new HighScoreRecord has been added to the high scores collection, it needs to be resorted and saved back to the file system.

The sortHighScoreDictionaryArray method is responsible for sorting the scores after a new record has been added to the collection. Using an NSSortDescriptor and the HighScoreRecord’s compare method, the scores are sorted on the totalScore property and a sorted array is returned.

Finally, saveLocalHighScores is called to persist the high scores collection. Its chief responsibility is serializing the HighScoreRecord objects to the NSDocumentDirectory.

Loading the Data

In the most common scenario you’ll be loading the high scores into a UITableView. The process is straightforward, amounting to deserializing the high scores file we’ve been saving.

The code in HighScoresViewController, which implements UITableViewDelegate and UITableViewDataSource, is quite standard. All the high score logic is handled by the other models, and populating a high scores display is simply a matter of making the right calls.

Note, since there won’t always be available local high score, cellForRowAtIndexPath first checks if a row exists and adds placeholder text if there aren’t enough scores to fill the table. In this case, we’ll set the UITableView to always show 10 high score slots with numberOfRowsInSection.

HighScoresViewController.h

#import <UIKit/UIKit.h>

#import "HighScoreCell.h"
#import "HighScoreRecord.h"
#import "HighScores.h"

@interface HighScoresViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
	IBOutlet UITableView *highScoreTable;
	NSArray *highScoreData;

}
@property (nonatomic, retain) UITableView *highScoreTable;
@property (nonatomic, retain) NSArray *highScoreData;

- (void) loadLocalHighScores;

@end

HighScoresViewController.m

#import "HighScoresViewController.h"

@implementation HighScoresViewController

@synthesize scoreTableSelector;
@synthesize highScoreTable;
@synthesize highScoreData;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        [self loadLocalHighScores];
    }
    return self;
}

. . .

- (void)loadLocalHighScores {
	highScoreData = [[HighScores getLocalHighScores] retain];
}

. . . 

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
	return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	HighScoreCell *cell = (HighScoreCell *)[tableView dequeueReusableCellWithIdentifier: @"HighScoreCellIdentifier"];
	if (cell == nil) {
		NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"HighScoreCell" owner:self options:nil];
		cell = (HighScoreCell *)[nib objectAtIndex:0];
	}
	NSUInteger row = [indexPath row];
	cell.rankLabel.text = [NSString stringWithFormat:@"%d", row+1];

	if (row < highScoreData.count){
		HighScoreRecord *record = (HighScoreRecord *)[highScoreData objectAtIndex:row];

		cell.nameLabel.text = record.name;
		cell.scoreLabel.text = [NSString stringWithFormat:@"%@", record.totalScore];

		NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
		[dateFormat setDateFormat:@"yyyy-MM-dd"];

		cell.dateLabel.text = [dateFormat stringFromDate:record.dateRecorded];
	} else {
		cell.nameLabel.text = @"-";
		cell.scoreLabel.text = @"-";
		cell.dateLabel.text = @"-";
	}

	return cell;
}

. . . 

@end

You can note again that the responsibility for loading and deserializing the local high scores file rests with the HighScores class’s getLocalHighScores method. This is called by the HighScoresViewController in loadLocalHighScores.

+ (NSMutableArray *)getLocalHighScores {
	NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[HighScores highScoresFilePath]];
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

	NSArray *highScores = [unarchiver decodeObjectForKey:@"HighScores"];

	return [[[NSMutableArray alloc] initWithArray:highScores copyItems:NO] autorelease];
}


Part 2

In the second part of this tutorial, I walk through setting up a simple web service in Rails that will act as an online leaderboard, and we’ll apply some basic authentication to secure the high score submissions. And with git and heroku.com’s free Rails hosting, deploying the service will be about as frictionless as web development gets.