cs193p – Assignment #3 Extra Task #2

Please note, this blog entry is from a previous course. You might want to check out the current one.

Add another tab to track the user’s high scores. You will want to use NSUserDefaults to store the high scores permanently. The tab might want to show information like the time the game was played and the game’s duration. It must also be clear which scores were Playing Card matching games and which scores were Set card matching games. Use attributes to highlight certain information (shortest game, highest score, etc.).

Let’s introduce a new model – which we actually borrow from last years course – which manages game results.

Its public interface needs a class method which returns an array of all available game scores, properties to hold the start time (date), the end time (date), the duration of the game, the score, and type of the game (set or playing cards). Finally it needs two helper methods which we will use to sort the game results by score and duration.

+ (NSArray *)allGameResults; // of GameResults
@property (readonly, nonatomic) NSDate *start;
@property (readonly, nonatomic) NSDate *end;
@property (readonly, nonatomic) NSTimeInterval duration;
@property (nonatomic) int score;
@property (strong, nonatomic) NSString *gameType;
- (NSComparisonResult)compareScore:(GameResult *)result;
- (NSComparisonResult)compareDuration:(GameResult *)result;

Because the start and the end date are publicly read only its necessary to make them privately writable:

@property (readwrite, nonatomic) NSDate *start;
@property (readwrite, nonatomic) NSDate *end;

Not so for the duration, because we generate it on the fly:

- (NSTimeInterval)duration
{
    return [self.end timeIntervalSinceDate:self.start];
}

Whenever a the score changes, we save the result to the user defaults (which in user-defaults terminology is called synchronize):

- (void)setScore:(int)score
{
    _score = score;
    self.end = [NSDate date];
    [self synchronize];
}

When we generate a new result the timer will “start running”:

- (id)init
{
    self = [super init];
    if (self) {
        _start = [NSDate date];
        _end = _start;
    }
    return self;
}

Now the “most complicated” part. The results are stored in a dictionary, which uses the start time/date as key (this way no two results will have the same key). Each entry of this dictionary is a property list holding the data of the result.

When synchronizing (equals “saving”) check if we have already data stored, if not create an empty set. Set the current data, put it back into the user defaults and synchronize …

Because it would be bad style otherwise, use constants as keys:

- (void)synchronize
{
    NSMutableDictionary *mutableGameResultsFromUserDefaults = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:ALL_RESULTS_KEY] mutableCopy];
    if (!mutableGameResultsFromUserDefaults)
        mutableGameResultsFromUserDefaults = [[NSMutableDictionary alloc] init];
    mutableGameResultsFromUserDefaults[[self.start description]] = [self asPropertyList];
    [[NSUserDefaults standardUserDefaults] setObject:mutableGameResultsFromUserDefaults
                                              forKey:ALL_RESULTS_KEY];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

The property list is actually just another dictionary, created by a helper method:

- (id)asPropertyList
{
    return @{ START_KEY : self.start, END_KEY : self.end, SCORE_KEY : @(self.score), GAME_KEY : self.gameType };
}

When reading back all results, we use another initialization method to create each single result, based on the stored property list/dictionary:

+ (NSArray *)allGameResults
{
    NSMutableArray *allGameResults = [[NSMutableArray alloc] init];    
    for (id plist in [[[NSUserDefaults standardUserDefaults] 
      dictionaryForKey:ALL_RESULTS_KEY] allValues]) {
        GameResult *result = [[GameResult alloc] initFromPropertyList:plist];
        [allGameResults addObject:result];
    }    
    return allGameResults;
}

The convenience initialization method takes every entry of the property list and fills the result properties:

- (id)initFromPropertyList:(id)plist
{
    self = [self init];
    if (self) {
        if ([plist isKindOfClass:[NSDictionary class]]) {
            NSDictionary *resultDictionary = (NSDictionary *)plist;
            _start = resultDictionary[START_KEY];
            _end = resultDictionary[END_KEY];
            _score = [resultDictionary[SCORE_KEY] intValue];
            _gameType = resultDictionary[GAME_KEY];
            if (!_start || !_end) self = nil;
        }
    }
    return self;
}

… the sorting helper methods just compare the appropriate properties of the results object:

- (NSComparisonResult)compareScore:(GameResult *)result
{
    return [@(self.score) compare:@(result.score)];
}

- (NSComparisonResult)compareDuration:(GameResult *)result
{
    return [@(self.duration) compare:@(result.duration)];
}

The game type needs to be set by the child game-view controllers, thus the parent needs a public property to store (and use it):

@property (strong, nonatomic) NSString *gameType;

Setting this property would be best when initializing the view controller, when its loaded, etc …
But the playing-card view controller has currently only a single method … and I am lazy just set it when creating a deck:

// PlayingCardGameViewController.m
- (Deck *)createDeck
{
    self.gameType = @"Playing Cards";
    ...
}

// SetCardGameViewController.m
- (Deck *)createDeck
{
    self.gameType = @"Set Cards";
    ...
}

The parent class needs a new property to hold the game result, which when instantiated lazily sets also the game type set by its child classes:

#import "GameResult.h"
...
@property (strong, nonatomic) GameResult *gameResult;
...
- (GameResult *)gameResult
{
    if (!_gameResult) _gameResult = [[GameResult alloc] init];
    _gameResult.gameType = self.gameType;
    return _gameResult;
}

Change the score of the game results when the user interface gets updated:

- (void)updateUI
{
    ...
    self.scoreLabel.text = [NSString stringWithFormat:@"Score: %d", self.game.score];
    self.gameResult.score = self.game.score;
    ...
}

… and reset the results, when a new deck is dealt:

- (IBAction)touchDealButton:(UIButton *)sender {
    ...
    self.gameResult = nil;
    ...
}

In storyboard add another view controller, link it to the tab-bar controller, name it and add an appropriate icon, and add a new text view:

cs193p – assignment #3 extra task #2 – high score view
cs193p – assignment #3 extra task #2 – high score view

Create a new view-controller class (note you might to use a better naming convention than I did to be more constant with the model) and link it to the new view controller previously created in storyboard.

Create an outlet for the text view, and an array to hold the game results (as model for this MVC):

#import "GameResult.h"
...
@property (weak, nonatomic) IBOutlet UITextView *scoresTextView;
@property (strong, nonatomic) NSArray *scores;

When a view will appear on screen (actually when the tab of the view controller gets selected), load all current game results and update the user interface:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.scores = [GameResult allGameResults];
    [self updateUI];
}

First create strings from all game results (we use a helper method for that) and use the final text to populate the text view. Then sort the game results using the helper method from the model and colorize the first and last result (which should be the highest and lowest score as well as the shortest and longest game) … the actual colorization is done using another helper method:

- (void)updateUI
{
    NSString *text = @"";
    for (GameResult *result in self.scores) {
        text = [text stringByAppendingString:[self stringFromResult:result]];
    }
    self.scoresTextView.text = text;    
    NSArray *sortedScores = [self.scores sortedArrayUsingSelector:@selector(compareScore:)];
    [self changeScore:[sortedScores firstObject] toColor:[UIColor redColor]];
    [self changeScore:[sortedScores lastObject] toColor:[UIColor greenColor]];
    sortedScores = [self.scores sortedArrayUsingSelector:@selector(compareDuration:)];
    [self changeScore:[sortedScores firstObject] toColor:[UIColor purpleColor]];
    [self changeScore:[sortedScores lastObject] toColor:[UIColor blueColor]];    
}

To create a string from a result it is not only necessary to define a format of where which property should appear, but also to format the dates to be readable:

- (NSString *)stringFromResult:(GameResult *)result
{
    return [NSString stringWithFormat:@"%@: %d, (%@, %gs)\n",
            result.gameType,
            result.score,
            [NSDateFormatter localizedStringFromDate:result.end
                                           dateStyle:NSDateFormatterShortStyle
                                           timeStyle:NSDateFormatterShortStyle],
            round(result.duration)];
}

For the colorization find the string of the result in the text view and change its color attribute:

- (void)changeScore:(GameResult *)result toColor:(UIColor *)color
{
    NSRange range = [self.scoresTextView.text rangeOfString:[self stringFromResult:result]];
    [self.scoresTextView.textStorage addAttribute:NSForegroundColorAttributeName
                                            value:color
                                            range:range];
}

The complete code is available on github.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

19 thoughts on “cs193p – Assignment #3 Extra Task #2”

  1. I’m not sure but seems like this way you are saving all scores and then sorting them, but the highscore should be saved only if beats previous highscore in the same game mode, or am I wrong?

    1. It’s not really specified, if only the one and only high score should be displayed, or if the display should be limited to a certain number of scores, etc. However, it says you should mark “shortest game, highest score”. That would not be reasonable displaying only a single high score.

      Nevertheless, don’t forget the goal this task is to train how to add another task, communicate data between tabs, using user defaults, displaying data using a text field, manipulating the layout of that text (coloring parts of it) … How you implement it, it’s up to your creativity 😉

  2. Hi,
    Could you explain this phrase:
    mutableGameResultsFromUserDefaults[[self.start description]] = [self asPropertyList];
    What’s the usage of [self.start description]?
    Thank you.

    1. the same question What’s the usage of [self.start description]?
      what is meant by dictionaryForKey:ALL_RESULTS_KEY] mutableCopy];
      sorry but this first time using dictionary , property list so i have some difficulty to understand the syntax , i read the notes and understand the logic but the syntax.

      1. start is an NSDate. I guess you could use NSDate as key for a dictionary – but some of the dictionary methods only take strings as key. Thus I used the description of the date – which is a string – as key.

  3. You set the end time in the score setter, so each time we click a card to set the score, the end time changes.
    But this is maybe not the “real” end time of the game.

    I try to make the scoresViewController as a subclass of CardGameViewController, set the “game” instance as public, and add this phrase self.gameResults.score = self.game.score in ViewWillAppear in scoresViewController to update the end time when scoresView appear.

    But this will initialize the “game” instance in the parent class CardGameViewController. One thing makes me confuse is that we can access the flipHistory in the subclass, why not the “game” instance?

    1. I am not sure I understand what you are trying to do. The scores view controller does not really have something to do with the card-game view controller. I think it’s better to create a new class, instead of having a super-power-parent class needing to handle everything.

      If you need to access the game property from subclasses of the card-game view controller, you need to add it to it’s public Interface (CardGameViewController.m)

      1. – (void)setScore:(int)score
        {
        _score = score;
        self.end = [NSDate date];
        [self synchronize];
        }
        You have put the self.end = [NSDate date]; in the score setter. And the self.gameResults.score = self.game.score is executed at cardGameViewController.

        So each time we click a card, we set a new score, also we change the end time. Then if we look at the scoresView tab, the end time will be fix at where we click the card.

        But in my opinion, this is not the real end time. So what I want is to set the end time where we click the scoresView tab buttons, in other words, in viewDIdAppear of scoresViewController.

        To do this, I move the line self.gameResults.score = self.game.score into viewDidAppear of scoresViewController. But game is an instance of cardGameViewController instead of scoresViewController, So I set the latter as the subclass of cardGameViewController to access game instance.

        For the reason I don’t know, it didn’t work. When the runtime executes the line self.gameResults.score = self.game.score, it just re-initialize the game instance.

      2. Ok, I finally put a copy of self.gameResults.score = self.game.score into viewWillDisappear of cardGameViewController, and it did what I want.

        I think the reason that it wasn’t working in the last situation (putting this line in viewWillAppear of scoresViewController) is properly the objective c has instance discontinuity when changing the views.

  4. + (NSArray *)allGameResults;
    why we make it class method not an array
    @property(strong , nonatomic) NSmutablearray* allGameResults;
    i have a miss understanding when we should use class method and when we can make it a property ?

    1. Honestly, that’s completely up to you. All solutions here are my personal point of view. I am not associated with Stanford in any way. Most likely my views will not be identical with the views of the instructor.

      In this case I used a class method, because I needed the game results, and the class method returns them as array. This way I have a single command. I don’t need to initiate the class. I don’t need to store that object. I don’t need to manipulate them later on using class specific commands. However, if you choose to solve the problem otherwise, there is no problem!

  5. every time we click the card the score changes so we save it to the score? and the end date is updated ? is that what happen
    i have logical miss understanding when we start the timer and when we stop it ?when we save the score of the game? from my point of view we shouldn’t change the end date and save the score every time we click the card
    can you please illustrate the logic of that point -set score , init-

    1. For the first part: yes, yes and yes
      For the second part, there is no actual timer (meaning NSTimer). I just store the start time at the beginning of new game, and the current (end) time whenever the score changes … and thus I can calculate the duration of the game by the subtracting those time properties.
      If the score changes every time you click on a card, then my procedure is equal to the one you suggested. Otherwise, the score does not change, and my code does not update the new end date, there your procedure might be of better …

      1. thanks for reply , but sorry i still have miss understanding so i will try to ask my questions in another way
        in set game for example the UI consist of : cards , label to show score , deal button , results tab bar button, OK
        every time we click card the score label changes , but it considered a temporarily change not the final score true ?
        did you change the score every time card is being clicked?
        if yes, is it legal – it should only store the final score of the game?
        if No, then when function setscore is called in your code so the final score of the game is set?
        what action on the game make you set the startDate?-init new game-
        what action on the game make you set the endDate?-call setscore-
        what happen when deal button is being clicked?
        sorry, I hope you do not get bored of my questions

        1. When you store only the final score, when do you know that the score is final? When Do you know a game has ended? What if the user closes the app? … If you adjust a score everytime it changes, it’s easier … Is it legal? … I don’t know any law which says otherwise, but I am not a lawyer 😉

  6. what is meant by
    for (GameResult *result in self.scores) {
    text = 1];
    }
    text is an string how you assign it a number “1” and why ?

Leave a Reply

Your email address will not be published.