8
\$\begingroup\$

I cooked up this little game today for the community challenge mentioned here. The idea of the game is that sometimes when Simon gives you a color, you should choose the opposite color (the color in the opposite corner). The game visually indicates this when it is an Opposite Simon. I'm not totally sure that the gameplay is "fun", but it was definitely fun to make!

First up, here is the game model:

SOGame.h

#import <Foundation/Foundation.h>
#import "SOGameStates.h"
#import "SOSimonSays.h"

@interface SOGame : NSObject

@property GameState state;
@property NSMutableArray *sequence;

-(void) update;
-(void) evaluateMove:(SimonSays)playerChoice;
-(void) restart;

@end

SOGame.m

#import "SOGame.h"
#import "SOSimon.h"

@implementation SOGame {
    int _selectionNumber;
}

-(id) init {
    self = [super init];
    if (self) {
        _state = GameStateStarted;
        _sequence = [[NSMutableArray alloc]init];
        _selectionNumber = 0;
    }
    return self;
}

#pragma mark - Game Rules
-(void) update {
    switch (self.state) {
        case GameStateStarted:
            [self createFirstSequence];
            break;
        case GameStateSuccess:
            [self createNewSequence];
            break;
        default:
            break;
    }
}
-(void) evaluateMove:(SimonSays)playerChoice {

    //if an invalid node is selected
    if (playerChoice == SimonSaysNumChoices) {
        return;
    }

    SOSimon *simon = [_sequence objectAtIndex:_selectionNumber];

    //different rules for opposite versus normal simons
    if (simon.opposite) {
        if (playerChoice == [self opposititePosition:simon.simonSays]) {
            [self doSuccessfulChoice];
        } else {
            _state = GameStateFailure;
        }
    } else {
        if (playerChoice == simon.simonSays) {
            [self doSuccessfulChoice];
        } else {
            _state = GameStateFailure;
        }
    }
}
-(void) doSuccessfulChoice {
    if (_selectionNumber < _sequence.count - 1) {
        _selectionNumber++;
    } else {
        _state = GameStateSuccess;
        _selectionNumber = 0;
    }
}
-(NSInteger) opposititePosition:(SimonSays)position {
    switch (position) {
        case SimonSaysTopLeft:
            return SimonSaysBottomRight;
        case SimonSaysTopRight:
            return SimonSaysBottomLeft;
        case SimonSaysBottomLeft:
            return SimonSaysTopRight;
        case SimonSaysBottomRight:
            return SimonSaysTopLeft;
        default:
            return 0;
    }
}
-(void) restart {
    _selectionNumber = 0;
    _sequence = nil;
    _state = GameStateStarted;
}

#pragma mark - Sequences
-(void) createFirstSequence {
    self.sequence = [self sequence:nil];
    self.state = GameStateSequenceReady;
}
-(void) createNewSequence {
    self.sequence = [self sequence:self.sequence];
    self.state = GameStateSequenceReady;
}
-(NSMutableArray *) sequence:(NSMutableArray *)oldSequence {
    NSMutableArray *newSequence = [[NSMutableArray alloc]init];
    if (oldSequence) {
        newSequence = [[NSMutableArray alloc]initWithArray:oldSequence];
    }
    [newSequence addObject:[[SOSimon alloc]init]];
    return newSequence;
}

@end

SOSimon.h

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

@interface SOSimon : NSObject

@property SimonSays simonSays;
@property BOOL opposite;

@end

SOSimon.m

#import "SOSimon.h"

static const int chanceForOpposite = 5;

@implementation SOSimon

-(instancetype) init {
    self = [super init];
    if (self) {
        _simonSays = arc4random_uniform(SimonSaysNumChoices);

        _opposite = NO;
        if (arc4random_uniform(100) < chanceForOpposite) {
            _opposite = YES;
        }
    }
    return self;
}

@end

SOSimonSays.h

typedef NS_ENUM(NSInteger, SimonSays){
    SimonSaysTopLeft = 0,
    SimonSaysTopRight,
    SimonSaysBottomLeft,
    SimonSaysBottomRight,
    SimonSaysNumChoices
};

SOGameStates.h

typedef NS_ENUM(NSInteger, GameState){
    GameStateStarted = 0,
    GameStateSequenceReady,
    GameStateSequencePlaying,
    GameStateWaitingForInput,
    GameStateSuccess,
    GameStateFailure,
    GameStateEnded
};

Now we have the SKScene. This is fully working at the moment, including animations when the sequence is played and when the player clicks one of the corners. The biggest challenge was to get the actions to play in sequence rather than all playing at the same time. To achieve this, I made a special class called SOQueuedAction:

SOQueuedAction.h

#import <Foundation/Foundation.h>
#import <SpriteKit/SpriteKit.h>
#import "SOSimon.h"

@interface SOQueuedAction : NSObject

-(id) initWithAction:(SKAction *)action node:(SKSpriteNode *)sprite simon:(SOSimon *)simon;

@property SKAction *action;
@property SKSpriteNode *sprite;
@property SOSimon *simon;

@end

SOQueuedAction.m

#import "SOQueuedAction.h"

@implementation SOQueuedAction

-(id) initWithAction:(SKAction *)action node:(SKSpriteNode *)sprite simon:(SOSimon *)simon {
    self = [super init];
    if (self) {
        _action = action;
        _sprite = sprite;
        _simon = simon;
    }
    return self;
}

@end

This basically just stores the action to perform and the sprite to perform it on, and then the game scene handles them one at a time:

SOGameScene.m

#import "SOGame.h"
#import "SOGameScene.h"
#import "SOQueuedAction.h"
#import "SOSimon.h"
#import "SOSimonSays.h"

NSString* const kNameTopLeft = @"topLeft";
NSString* const kNameTopRight = @"topRight";
NSString* const kNameBottomLeft = @"bottomLeft";
NSString* const kNameBottomRight = @"bottomRight";

@implementation SOGameScene {
    CGSize _initialScreenSize;

    SOGame *_game;
    SKNode *_gameBoard;
    SKNode *_overlayNode;

    SKLabelNode *_turnLabel;

    NSMutableArray *_actionQueue;
    BOOL _isActionPlaying;
}

#pragma mark - Initialization
-(void) didMoveToView:(SKView *)view {
    _actionQueue = [[NSMutableArray alloc]init];

    _initialScreenSize = self.frame.size;
    self.backgroundColor = [SKColor whiteColor];

    _game = [[SOGame alloc]init];

    [self setUpGameBoard];

    [self setUpLabels];

    _overlayNode = [[SKNode alloc]init];
    [self addChild:_overlayNode];
}
-(void) setUpGameBoard {
    _gameBoard = [[SKNode alloc]init];
    [self addChild:_gameBoard];

    float widthDivisor = 3.05;
    float heightDivisor = 2.59;
    CGPoint topLeftPos = CGPointMake(_initialScreenSize.width/widthDivisor,
                                     _initialScreenSize.height - _initialScreenSize.height/heightDivisor);
    CGPoint topRightPos = CGPointMake(_initialScreenSize.width - _initialScreenSize.width/widthDivisor,
                                      _initialScreenSize.height - _initialScreenSize.height/heightDivisor);
    CGPoint bottomLeftPos = CGPointMake(_initialScreenSize.width/widthDivisor,
                                        _initialScreenSize.height/heightDivisor);
    CGPoint bottomRightPos = CGPointMake(_initialScreenSize.width - _initialScreenSize.width/widthDivisor,
                                         _initialScreenSize.height/heightDivisor);

    SKSpriteNode *pieceTopLeft = [self gamePieceForColor:[SKColor greenColor] position:SimonSaysTopLeft];
    pieceTopLeft.position = topLeftPos;
    [_gameBoard addChild:pieceTopLeft];

    SKSpriteNode *pieceTopRight = [self gamePieceForColor:[SKColor yellowColor] position:SimonSaysTopRight];
    pieceTopRight.position = topRightPos;
    [_gameBoard addChild:pieceTopRight];

    SKSpriteNode *pieceBottomLeft = [self gamePieceForColor:[SKColor redColor] position:SimonSaysBottomLeft];
    pieceBottomLeft.position = bottomLeftPos;
    [_gameBoard addChild:pieceBottomLeft];

    SKSpriteNode *pieceBottomRight = [self gamePieceForColor:[SKColor blueColor] position:SimonSaysBottomRight];
    pieceBottomRight.position = bottomRightPos;
    [_gameBoard addChild:pieceBottomRight];
}
-(NSString *) nameForPosition:(SimonSays)position {
    switch (position) {
        case SimonSaysTopLeft:
            return kNameTopLeft;
        case SimonSaysTopRight:
            return kNameTopRight;
        case SimonSaysBottomLeft:
            return kNameBottomLeft;
        case SimonSaysBottomRight:
            return kNameBottomRight;
        default:
            return @"Invalid Position";
    }
}
-(void) setUpLabels {
    SKLabelNode *titleLabelFirstLine = [self titleLabel:@"Simon's"];
    titleLabelFirstLine.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height - _initialScreenSize.height/10);
    [self addChild:titleLabelFirstLine];

    SKLabelNode *titleLabelSecondLine = [self titleLabel:@"Opposites"];
    titleLabelSecondLine.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height - _initialScreenSize.height/6);
    [self addChild:titleLabelSecondLine];

    SKLabelNode *turnWordLabel = [self defaultSizeLabel:@"Turn"];
    turnWordLabel.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/6);
    [self addChild:turnWordLabel];

    _turnLabel = [self defaultSizeLabel:[NSString stringWithFormat:@"%lu", (unsigned long)_game.sequence.count]];
    _turnLabel.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/10);
    [self addChild:_turnLabel];
}

#pragma mark - Game Loop
-(void)update:(CFTimeInterval)currentTime {

    [_game update];

    if (_game.state == GameStateSequenceReady) {
        [self createActionSequence];
    } else if (_game.state == GameStateSequencePlaying) {
        [self playActionSequence];
    } else if (_game.state == GameStateFailure) {
        SKLabelNode *message = [self titleLabel:@"Failed!"];
        message.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
        [_overlayNode addChild:message];

        SKSpriteNode *retryButton = [self retryButton];
        retryButton.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/3);
        [_overlayNode addChild:retryButton];
    }

    _turnLabel.text = [NSString stringWithFormat:@"%lu", (unsigned long)_game.sequence.count];
}

#pragma mark - Touch Controls
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *touch in touches) {
        if(touch.tapCount == 1) {
            CGPoint positionInScene = [touch locationInNode:self];
            [self selectNodeForTouch:positionInScene];
        }
    }
}
-(void)selectNodeForTouch:(CGPoint)touchLocation {
    if (_game.state == GameStateWaitingForInput) {
        SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
        [_game evaluateMove:[self simonSaysForName:touchedNode.name]];
        [self doQuickFadeAnimation:touchedNode];
    } else {
        SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
        if ([touchedNode.name isEqualToString:@"retryButton"]) {
            [_game restart];
            [_overlayNode removeAllChildren];
        }
    }
}
-(SimonSays) simonSaysForName:(NSString *)name {
    if ([name isEqualToString:kNameTopLeft]) {
        return SimonSaysTopLeft;
    } else if ([name isEqualToString:kNameTopRight]) {
        return SimonSaysTopRight;
    } else if ([name isEqualToString:kNameBottomLeft]) {
        return SimonSaysBottomLeft;
    } else if ([name isEqualToString:kNameBottomRight]) {
        return SimonSaysBottomRight;
    }
    return SimonSaysNumChoices;
}

#pragma mark - Animations
-(void) createActionSequence {

    for (SOSimon *simon in _game.sequence) {

        SKSpriteNode *nodeToAnimate = nil;
        switch (simon.simonSays) {
            case SimonSaysTopLeft:
                nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameTopLeft];
                break;
            case SimonSaysTopRight:
                nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameTopRight];
                break;
            case SimonSaysBottomLeft:
                nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameBottomLeft];
                break;
            case SimonSaysBottomRight:
                nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameBottomRight];
                break;
            default:
                break;
        }

        if (nodeToAnimate) {
            SKAction *fadeOutAndIn = [SKAction sequence:@[[SKAction colorizeWithColor:[SKColor whiteColor]
                                                                 colorBlendFactor:0.5 duration:1],
                                                          [SKAction colorizeWithColor:nodeToAnimate.color
                                                                 colorBlendFactor:0.5 duration:1]
                                                          ]];
            SOQueuedAction *actionForQueue = [[SOQueuedAction alloc]initWithAction:fadeOutAndIn
                                                                          node:nodeToAnimate
                                                                         simon:simon];
            [_actionQueue addObject:actionForQueue];
        }
    }
    _game.state = GameStateSequencePlaying;
}
-(void) playActionSequence {
    if (!_isActionPlaying) {
        if (_actionQueue.count > 0) {
            _isActionPlaying = YES;

            SOQueuedAction *action = [_actionQueue firstObject];

            if (action.simon.opposite) {
                [action.sprite addChild:[[SKSpriteNode alloc]initWithColor:[SKColor blackColor] size:CGSizeMake(action.sprite.size.width*0.85, action.sprite.size.height*0.85)]];
            }

            [action.sprite runAction:action.action completion:^{
                [action.sprite removeAllChildren];
                _isActionPlaying = NO;
                if (_actionQueue.count == 0) {
                    _game.state = GameStateWaitingForInput;
                    [self playActionSequence];
                }
            }];

            [_actionQueue removeObjectAtIndex:0];
        }
    }
}
-(void) doQuickFadeAnimation:(SKSpriteNode *)sprite {
    SKColor *originalColor = sprite.color;
    [sprite runAction:[SKAction colorizeWithColor:[SKColor whiteColor]
                             colorBlendFactor:0.5 duration:0.2] completion:^ {
        [sprite runAction:[SKAction colorizeWithColor:originalColor
                                 colorBlendFactor:0.5 duration:0.2]];
    }];
}

#pragma mark - UI Elements
-(SKLabelNode *) titleLabel:(NSString *)text {
    SKLabelNode *titleLabel = [[SKLabelNode alloc]initWithFontNamed:@"Arial"];
    titleLabel.text = text;
    titleLabel.fontSize = 30;
    titleLabel.fontColor = [SKColor blackColor];
    return titleLabel;
}
-(SKLabelNode *) defaultSizeLabel:(NSString *)text {
    SKLabelNode *defaultSizeLabel = [[SKLabelNode alloc]initWithFontNamed:@"Arial"];
    defaultSizeLabel.text = text;
    defaultSizeLabel.fontColor = [SKColor blackColor];
    return defaultSizeLabel;
}
-(SKSpriteNode *) gamePieceForColor:(SKColor *)color position:(SimonSays)position {
    SKSpriteNode *gamePiece = [[SKSpriteNode alloc]initWithColor:color size:CGSizeMake(_initialScreenSize.width/3, _initialScreenSize.width/3)];
    gamePiece.name = [self nameForPosition:position];
    return gamePiece;
}
-(SKSpriteNode *) retryButton {
    SKSpriteNode *retryButton = [[SKSpriteNode alloc]initWithColor:[SKColor purpleColor] size:CGSizeMake(_initialScreenSize.width/2, _initialScreenSize.height/5)];
    retryButton.name = @"retryButton";

    SKLabelNode *retryLabel = [self defaultSizeLabel:@"Retry?"];
    retryLabel.name = @"retryButton";
    [retryButton addChild:retryLabel];

    return retryButton;
}

@end

I think that my code is pretty clean overall, but I am always open to any kind of criticism. If there are any huge problems, they are probably in the GameScene class.

Here is a screenshot of the game: Simon's Opposites

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$
@property NSMutableArray *sequence;

So, I want to just look at this property on our class. As it stands, it's a publicly available property on our class. There's no reason someone couldn't come along and write this code:

[soGameInstance.sequence removeAllObjects];
[soGameInstance.sequence addObject:[NSNull null]];
[soGameInstance evaluateMove:SimonSaysTopLeft];

And now you're getting an exception that sounds something like this:

NSNull does not respond to selector 'getSimonSays'

I've only skimmed through the rest of your code (because this publicly exposed mutable array bothers me that much), so I'm not sure how exactly this array is accessed, if at all, by your current code, but does it even need to be?

We do need an internal array to keep track of the move sequence, that is true. But, in terms of external access to that array?

We could offer a readonly immutable array:

@property (readonly) NSArray *sequence;

Which does nothing more than return a copy of the internal sequence array:

- (NSArray *)sequence {
     return [NSArray arrayWithArray:_sequence];
}

(Where we've declared NSMutableArray *_sequence; in our .m file).

OR, I might like it better if we just offer a method for fetching a particular sequence number:

- (SOSimon *)simonSaysAtIndex:(NSUInteger)index {
    return (_sequence.count > index) ? _sequence[index] : nil;
}

One other small note:

-(NSMutableArray *) sequence:(NSMutableArray *)oldSequence {
    NSMutableArray *newSequence = [[NSMutableArray alloc]init];
    if (oldSequence) {
        newSequence = [[NSMutableArray alloc]initWithArray:oldSequence];
    }
    [newSequence addObject:[[SOSimon alloc]init]];
    return newSequence;
}

Here, we're unnecessarily double allocating an array any time oldSequence is non-nil. We should instead do this:

NSMutableArray *newSequence;
if (oldSequence) {
    newSequence = [NSMutableArray arrayWithArray:oldSequence];
} else {
    newSequence = [NSMutableArray array];
}
[newSequence addObject:[[SOSimon alloc] init]];
return newSequence;

Although, why can't we just do this?

if (!oldSequence) {
    oldSequence = [NSMutableArray array];
}
[oldSequence addObject:[[SOSimon alloc] init]];
return oldSequence;

Either way, this method actually seems a little strange.

\$\endgroup\$
2
\$\begingroup\$

SOGame.m class

Validation logic can be improved:

// Use shorter syntax to access array elements
SOSimon *simon = _sequence[_selectionNumber];

// Remove duplicated code
NSUInteger correctChoice = simon.opposite ? [self opposititePosition:simon.simonSays] : simon.simonSays;

if (playerChoice == correctChoice) {
    [self doSuccessfulChoice];
} else {
    _state = GameStateFailure;
}

SOSimonSays.h

Putting SimonSaysNumChoices inside your enum will confuse auto-complete and warnings system down the road. Create a separate constant for it.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.