Files
ResearchKit/ResearchKitActiveTask/Spatial Span Memory/ORKSpatialSpanMemoryStepViewController.m
Pariece McKinney b14e5cfcb0 Public Release 3.0
2024-03-28 19:39:04 -04:00

784 lines
30 KiB
Objective-C

/*
Copyright (c) 2015, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "ORKSpatialSpanMemoryStepViewController.h"
#import "ORKActiveStepView.h"
#import "ORKStepContainerView_Private.h"
#import "ORKSpatialSpanMemoryContentView.h"
#import "ORKVerticalContainerView_Internal.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKStepViewController_Internal.h"
#import "ORKStepHeaderView_Internal.h"
#import "ORKActiveStep_Internal.h"
#import "ORKCollectionResult_Private.h"
#import "ORKSpatialSpanMemoryResult.h"
#import "ORKStep_Private.h"
#import "ORKSpatialSpanGame.h"
#import "ORKSpatialSpanGameState.h"
#import "ORKSpatialSpanMemoryStep.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKHelpers_Internal.h"
#import "ORKSkin.h"
#import <QuartzCore/CABase.h>
static const NSTimeInterval MemoryGameActivityTimeout = 20;
typedef NS_ENUM(NSInteger, ORKSpatialSpanStepState) {
ORKSpatialSpanStepStateInitial,
ORKSpatialSpanStepStatePlayback,
ORKSpatialSpanStepStateGameplay,
ORKSpatialSpanStepStateTimeout,
ORKSpatialSpanStepStateFailed,
ORKSpatialSpanStepStateSuccess,
ORKSpatialSpanStepStateRestart,
ORKSpatialSpanStepStateComplete,
ORKSpatialSpanStepStateStopped,
ORKSpatialSpanStepStatePaused
};
@class ORKState;
typedef void (^_ORKStateHandler)(ORKState *fromState, ORKState *_toState, id context);
// Poor man's state machine:
// Define entry and exit handlers for each state.
// Transitions are a free for all!
@interface ORKState : NSObject
+ (ORKState *)stateWithState:(NSInteger)state entryHandler:(_ORKStateHandler)entryHandler exitHandler:(_ORKStateHandler)exitHandler context:(id)context;
@property (nonatomic, assign) NSInteger state;
@property (nonatomic, weak) id context;
@property (nonatomic, copy) _ORKStateHandler entryHandler;
- (void)setEntryHandler:(_ORKStateHandler)entryHandler;
@property (nonatomic, copy) _ORKStateHandler exitHandler;
- (void)setExitHandler:(_ORKStateHandler)exitHandler;
@end
@implementation ORKState
+ (ORKState *)stateWithState:(NSInteger)state entryHandler:(_ORKStateHandler)entryHandler exitHandler:(_ORKStateHandler)exitHandler context:(id)context {
ORKState *s = [ORKState new];
s.state = state;
s.entryHandler = entryHandler;
s.exitHandler = exitHandler;
s.context = context;
return s;
}
@end
@interface ORKSpatialSpanMemoryStepViewController () <ORKSpatialSpanMemoryGameViewDelegate>
@end
@implementation ORKSpatialSpanMemoryStepViewController {
ORKSpatialSpanMemoryContentView *_contentView;
ORKState *_state;
NSDictionary *_states;
ORKGridSize _gridSize;
ORKSpatialSpanGameState *_currentGameState;
UIBarButtonItem *_customLearnMoreButtonItem;
UIBarButtonItem *_learnMoreButtonItem;
// ORKSpatialSpanMemoryGameRecord
NSMutableArray *_gameRecords;
NSTimeInterval _gameStartTime;
NSInteger _lastRoundScore;
NSInteger _playbackIndex;
NSInteger _score;
NSInteger _numberOfItems;
NSInteger _gamesCounter;
NSInteger _consecutiveGamesFailed;
NSInteger _nextGameSequenceLength;
NSTimer *_playbackTimer;
NSTimer *_activityTimer;
}
- (ORKSpatialSpanMemoryStep *)spatialSpanStep {
return (ORKSpatialSpanMemoryStep *)self.step;
}
- (instancetype)initWithStep:(ORKStep *)step {
self = [super initWithStep:step];
if (self) {
self.suspendIfInactive = YES;
}
return self;
}
#pragma mark Overrides
- (void)viewDidLoad {
// Setup to always have a learn more button item but with an empty title
BOOL usesDefaultCopyright = (self.learnMoreButtonItem == nil);
if (usesDefaultCopyright) {
self.learnMoreButtonItem = [[UIBarButtonItem alloc] initWithTitle:ORKLocalizedString(@"BUTTON_COPYRIGHT", nil) style:UIBarButtonItemStylePlain target:self action:@selector(showCopyright)];
}
[super viewDidLoad];
_contentView = [ORKSpatialSpanMemoryContentView new];
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
_contentView.footerHidden = YES;
_contentView.gameView.delegate = self;
self.activeStepView.activeCustomView = _contentView;
self.activeStepView.customContentFillsAvailableSpace = YES;
[self resetUI];
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleUserTap:)];
[self.activeStepView addGestureRecognizer:tapGestureRecognizer];
}
- (void)stepDidChange {
[super stepDidChange];
[self initializeStates];
}
- (void)start {
[super start];
if (!_state) {
[self transitionToState:ORKSpatialSpanStepStateInitial];
}
[self transitionToState:ORKSpatialSpanStepStatePlayback];
}
- (void)suspend {
[super suspend];
switch (_state.state) {
case ORKSpatialSpanStepStatePlayback:
case ORKSpatialSpanStepStateGameplay:
[self transitionToState:ORKSpatialSpanStepStatePaused];
break;
default:
break;
}
}
- (void)resume {
[super resume];
}
- (void)finish {
[super finish];
[self transitionToState:ORKSpatialSpanStepStateStopped];
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [super result];
// "Now" is the end time of the result, which is either actually now,
// or the last time we were in the responder chain.
NSDate *now = stepResult.endDate;
NSMutableArray *results = [NSMutableArray arrayWithArray:stepResult.results];
ORKSpatialSpanMemoryResult *memoryResult = [[ORKSpatialSpanMemoryResult alloc] initWithIdentifier:self.step.identifier];
memoryResult.startDate = stepResult.startDate;
memoryResult.endDate = now;
NSMutableArray *records = [NSMutableArray new];
__block NSInteger numberOfFailures = 0;
__block NSInteger score = 0;
// Only include valid records
[_gameRecords enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
ORKSpatialSpanMemoryGameRecord *record = (ORKSpatialSpanMemoryGameRecord *)obj;
if (record.gameStatus != ORKSpatialSpanMemoryGameStatusUnknown) {
[records addObject:record];
score += record.score;
if (record.gameStatus != ORKSpatialSpanMemoryGameStatusSuccess) {
numberOfFailures++;
}
}
}];
memoryResult.score = score;
memoryResult.numberOfFailures = numberOfFailures;
memoryResult.numberOfGames = records.count;
memoryResult.gameRecords = [records copy];
[results addObject:memoryResult];
stepResult.results = [results copy];
return stepResult;
}
#pragma mark UpdateGameRecord
- (ORKSpatialSpanMemoryGameRecord *)currentGameRecord {
return _gameRecords ? _gameRecords.lastObject : nil;
}
- (void)createGameRecord {
if (_gameRecords == nil) {
_gameRecords = [NSMutableArray new];
}
ORKSpatialSpanMemoryGameRecord *gameRecord = [ORKSpatialSpanMemoryGameRecord new];
gameRecord.seed = _currentGameState.game.seed;
gameRecord.gameSize = _currentGameState.game.gameSize;
NSMutableArray *targetSequence = [NSMutableArray new];
[_currentGameState.game enumerateSequenceWithHandler:^(NSInteger step, NSInteger tileIndex, BOOL isLastStep, BOOL *stop) {
[targetSequence addObject:@(tileIndex)];
}];
gameRecord.sequence = [targetSequence copy];
[_gameRecords addObject:gameRecord];
_lastRoundScore = _score;
}
- (void)updateGameRecordTargetRects {
ORKSpatialSpanMemoryGameRecord *record = [self currentGameRecord];
NSArray *tileViews = _contentView.gameView.tileViews;
NSMutableArray *targetRects = [NSMutableArray new];
for (UIView *tileView in tileViews) {
CGRect rect = [self.view convertRect:tileView.frame fromView:tileView.superview];
[targetRects addObject:[NSValue valueWithCGRect:rect]];
}
record.targetRects = [targetRects copy];
NSAssert(tileViews.count == 0 || tileViews.count == record.gameSize, nil);
}
- (void)updateGameRecordOnStartingGamePlay {
_gameStartTime = CACurrentMediaTime();
}
- (void)handleUserTap:(UITapGestureRecognizer *)tapRecognizer {
if (_state.state != ORKSpatialSpanStepStateGameplay) {
return;
}
[self updateGameRecordOnTouch:-1 location:[tapRecognizer locationInView: self.view]];
}
- (void)updateGameRecordOnTouch:(NSInteger)targetIndex location:(CGPoint)location {
ORKSpatialSpanMemoryGameTouchSample *sample = [ORKSpatialSpanMemoryGameTouchSample new];
sample.timestamp = CACurrentMediaTime() - _gameStartTime;
sample.location = location;
sample.targetIndex = targetIndex;
ORKSpatialSpanMemoryGameRecord *record = [self currentGameRecord];
NSAssert(record, nil);
NSInteger currentStep = 0;
for (ORKSpatialSpanMemoryGameTouchSample *aSample in record.touchSamples) {
if (aSample.isCorrect) {
currentStep++;
}
}
sample.correct = (targetIndex == [_currentGameState.game tileIndexForStep:currentStep]);
NSMutableArray *sampleArray = [NSMutableArray arrayWithArray:record.touchSamples];
[sampleArray addObject:sample];
record.touchSamples = [sampleArray copy];
}
- (void)updateGameRecordOnSuccess {
[self currentGameRecord].gameStatus = ORKSpatialSpanMemoryGameStatusSuccess;
}
- (void)updateGameRecordOnFailure {
[self currentGameRecord].gameStatus = ORKSpatialSpanMemoryGameStatusFailure;
}
- (void)updateGameRecordOnTimeout {
[self currentGameRecord].gameStatus = ORKSpatialSpanMemoryGameStatusTimeout;
}
- (void)updateGameRecordScore {
[self currentGameRecord].score = _score - _lastRoundScore;
}
- (void)updateGameRecordOnPause {
[self currentGameRecord].gameStatus = ORKSpatialSpanMemoryGameStatusUnknown;
}
#pragma mark ORKSpatialSpanStepStateInitial
- (ORKGridSize)gridSizeForSpan:(NSInteger)span {
NSInteger numberOfGridEntriesDesired = span * 2;
NSInteger value = (NSInteger)ceil(sqrt(numberOfGridEntriesDesired));
value = MAX(value, 2);
value = MIN(value, 6);
return (ORKGridSize){value,value};
}
- (void)resetGameAndUI {
_score = 0;
_numberOfItems = 0;
_gamesCounter = 0;
_consecutiveGamesFailed = 0;
ORKSpatialSpanMemoryStep *step = [self spatialSpanStep];
_nextGameSequenceLength = step.initialSpan;
[self resetForNewGame];
}
- (void)resetUI {
_contentView.numberOfItems = _score;
_contentView.score = _numberOfItems;
_contentView.footerHidden = YES;
_contentView.buttonItem = nil;
_contentView.gameView.gridSize = _gridSize;
_contentView.gameView.customTargetImage = [[self spatialSpanStep] customTargetImage];
}
- (void)resetForNewGame {
[self.activeStepView updateTitle:nil text:self.step.text];
_numberOfItems = 0;
NSInteger sequenceLength = _nextGameSequenceLength;
_gridSize = [self gridSizeForSpan:sequenceLength];
ORKSpatialSpanGame *game = [[ORKSpatialSpanGame alloc] initWithGameSize:_gridSize.width * _gridSize.height sequenceLength:sequenceLength seed:0];
ORKSpatialSpanGameState *gameState = [[ORKSpatialSpanGameState alloc] initWithGame:game];
_currentGameState = gameState;
[self createGameRecord];
[self resetUI];
}
#pragma mark ORKSpatialSpanStepStatePlayback
- (void)applyTargetState:(ORKSpatialSpanTargetState)targetState toSequenceIndex:(NSInteger)index duration:(NSTimeInterval)duration {
ORKSpatialSpanGame *game = _currentGameState.game;
if (index == NSNotFound || index < 0 || index >= game.sequenceLength ) {
return;
}
NSInteger tileIndex = [game tileIndexForStep:index];
ORKSpatialSpanMemoryGameView *gameView = _contentView.gameView;
[gameView setState:targetState forTileIndex:tileIndex animated:YES];
if (duration > 0 && targetState != ORKSpatialSpanTargetStateQuiescent) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self applyTargetState:ORKSpatialSpanTargetStateQuiescent toSequenceIndex:index duration:0];
});
}
}
- (void)playbackNextItem {
const NSInteger sequenceLength = _currentGameState.game.sequenceLength;
if (_playbackIndex >= sequenceLength) {
[self transitionToState:ORKSpatialSpanStepStateGameplay];
} else {
ORKSpatialSpanMemoryStep *step = [self spatialSpanStep];
NSInteger index = _playbackIndex;
NSInteger previousIndex = index - 1;
if (step.requireReversal) {
// Play the indexes in reverse order when we require reversal. The participant
// is then required to tap the sequence in the forward direction, which
// appears as a reversal to them.
index = sequenceLength - 1 - index;
previousIndex = sequenceLength - 1 - previousIndex;
}
// Make sure the previous step *is *cleared
[self applyTargetState:ORKSpatialSpanTargetStateQuiescent toSequenceIndex:previousIndex duration:0];
// The active display should be visible for half the timer interval
[self applyTargetState:ORKSpatialSpanTargetStateActive toSequenceIndex:index duration:(step.playSpeed / 2)];
}
_playbackIndex++;
}
- (void)startPlayback {
_playbackIndex = 0;
_contentView.footerHidden = YES;
_contentView.buttonItem = nil;
ORKSpatialSpanMemoryStep *step = [self spatialSpanStep];
NSString *title = [NSString localizedStringWithFormat:ORKLocalizedString(@"MEMORY_GAME_PLAYBACK_TITLE_%@", nil), step.customTargetPluralName ? : ORKLocalizedString(@"SPATIAL_SPAN_MEMORY_TARGET_PLURAL", nil)];
[self.activeStepView updateTitle:title text:nil];
[_contentView.gameView resetTilesAnimated:NO];
_playbackTimer = [NSTimer scheduledTimerWithTimeInterval:step.playSpeed target:self selector:@selector(playbackNextItem) userInfo:nil repeats:YES];
}
- (void)finishPlayback {
[_playbackTimer invalidate];
_playbackTimer = nil;
}
#pragma mark ORKSpatialSpanStepStateGameplay
- (void)setNumberOfItems:(NSInteger)numberOfItems {
_numberOfItems = numberOfItems;
[_contentView setNumberOfItems:_numberOfItems];
}
- (void)setScore:(NSInteger)score {
_score = score;
[_contentView setScore:_score];
[self updateGameRecordScore];
}
- (void)activityTimeout {
[self transitionToState:ORKSpatialSpanStepStateTimeout];
}
- (void)resetActivityTimer {
[_activityTimer invalidate];
_activityTimer = nil;
_activityTimer = [NSTimer scheduledTimerWithTimeInterval:MemoryGameActivityTimeout target:self selector:@selector(activityTimeout) userInfo:nil repeats:NO];
}
- (void)startGameplay {
[self setNumberOfItems:0];
// Update the target rects here, since layout will be complete by this point.
[self updateGameRecordTargetRects];
_contentView.footerHidden = NO;
_contentView.buttonItem = nil;
ORKSpatialSpanMemoryStep *step = [self spatialSpanStep];
NSString *pluralItemName = step.customTargetPluralName ? : ORKLocalizedString(@"SPATIAL_SPAN_MEMORY_TARGET_PLURAL", nil);
NSString *standaloneItemName = step.customTargetPluralName ? : ORKLocalizedString(@"SPATIAL_SPAN_MEMORY_TARGET_STANDALONE", nil);
_contentView.capitalizedPluralItemDescription = [standaloneItemName capitalizedStringWithLocale:[NSLocale currentLocale]];
NSString *titleFormat = step.requireReversal ? ORKLocalizedString(@"MEMORY_GAME_GAMEPLAY_REVERSE_TITLE_%@", nil) : ORKLocalizedString(@"MEMORY_GAME_GAMEPLAY_TITLE_%@", nil);
NSString *title = [NSString stringWithFormat:titleFormat, pluralItemName];
[self.activeStepView updateTitle:title text:nil];
[self resetActivityTimer];
// Ensure tiles are all reset at this point
[_currentGameState reset];
[_contentView.gameView resetTilesAnimated:NO];
[_contentView setScore:_score];
[_contentView setNumberOfItems:_numberOfItems];
[self updateGameRecordOnStartingGamePlay];
}
- (void)finishGameplay {
[_activityTimer invalidate];
_activityTimer = nil;
}
- (void)gameView:(ORKSpatialSpanMemoryGameView *)gameView didTapTileWithIndex:(NSInteger)tileIndex recognizer:(UITapGestureRecognizer *)recognizer {
if (_state.state != ORKSpatialSpanStepStateGameplay) {
return;
}
[self updateGameRecordOnTouch:tileIndex location:[recognizer locationInView:self.view]];
ORKSpatialSpanResult result = [_currentGameState playTileIndex:tileIndex];
switch (result) {
case ORKSpatialSpanResultIgnore:
break;
case ORKSpatialSpanResultCorrect:
[gameView setState:ORKSpatialSpanTargetStateCorrect forTileIndex:tileIndex animated:YES];
NSInteger stepIndex = [_currentGameState currentStepIndex];
[self setNumberOfItems:_numberOfItems + 1];
[self setScore:_score + (round(log2(stepIndex)) + 1) * 5];
[self resetActivityTimer];
if ([_currentGameState isComplete]) {
[self transitionToState:ORKSpatialSpanStepStateSuccess];
}
break;
case ORKSpatialSpanResultIncorrect:
[gameView setState:ORKSpatialSpanTargetStateIncorrect forTileIndex:tileIndex animated:YES];
[self transitionToState:ORKSpatialSpanStepStateFailed];
break;
}
}
#pragma mark ORKSpatialSpanStepStateSuccess
- (void)updateGameCountersForSuccess:(BOOL)success {
ORKSpatialSpanMemoryStep *step = [self spatialSpanStep];
if (success) {
NSInteger sequenceLength = [_currentGameState.game sequenceLength];
[self setScore:_score + (round(log2(sequenceLength)) + 1) * 5];
_gamesCounter++;
_consecutiveGamesFailed = 0;
_nextGameSequenceLength = MIN(_nextGameSequenceLength + 1, step.maximumSpan);
} else {
_gamesCounter++;
_consecutiveGamesFailed++;
_nextGameSequenceLength = MAX(_nextGameSequenceLength - 1, step.minimumSpan);
}
}
- (void)continueAction {
ORKSpatialSpanMemoryStep *step = [self spatialSpanStep];
if (_gamesCounter < step.maximumTests && _consecutiveGamesFailed < step.maximumConsecutiveFailures) {
// Generate a new game
[self transitionToState:ORKSpatialSpanStepStateRestart];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
} else {
[self finish];
}
}
- (void)showSuccess {
[self updateGameRecordOnSuccess];
[self updateGameCountersForSuccess:YES];
if ([self finishIfCompletedGames]) {
return;
}
[self.activeStepView updateTitle:ORKLocalizedString(@"MEMORY_GAME_COMPLETE_TITLE", nil) text:ORKLocalizedString(@"MEMORY_GAME_COMPLETE_MESSAGE", nil)];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.75 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self->_contentView.buttonItem = [ORKBorderedButton new];
[self->_contentView.buttonItem setTitle:ORKLocalizedString(@"BUTTON_NEXT", nil) forState:UIControlStateNormal];
[self->_contentView.buttonItem addTarget:self action:@selector(continueAction) forControlEvents:UIControlEventTouchUpInside];
});
}
#pragma mark ORKSpatialSpanStepStateFailed
- (void)tryAgainAction {
// Restart with a new, shorter game
[self transitionToState:ORKSpatialSpanStepStateRestart];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
}
- (BOOL)finishIfCompletedGames {
ORKSpatialSpanMemoryStep *step = [self spatialSpanStep];
if (_consecutiveGamesFailed >= step.maximumConsecutiveFailures || _gamesCounter >= step.maximumTests) {
[self transitionToState:ORKSpatialSpanStepStateComplete];
return YES;
}
return NO;
}
- (void)showFailed {
[self updateGameRecordOnFailure];
[self updateGameCountersForSuccess:NO];
if ([self finishIfCompletedGames]) {
return;
}
[self.activeStepView updateTitle:ORKLocalizedString(@"MEMORY_GAME_FAILED_TITLE", nil) text:ORKLocalizedString(@"MEMORY_GAME_FAILED_MESSAGE", nil)];
_contentView.buttonItem = [ORKBorderedButton new];
[_contentView.buttonItem setTitle:ORKLocalizedString(@"BUTTON_NEXT", nil) forState:UIControlStateNormal];
[_contentView.buttonItem addTarget:self action:@selector(tryAgainAction) forControlEvents:UIControlEventTouchUpInside];
}
#pragma mark ORKSpatialSpanStepStateTimeout
- (void)showTimeout {
[self updateGameRecordOnTimeout];
[self updateGameCountersForSuccess:NO];
if ([self finishIfCompletedGames]) {
return;
}
[self.activeStepView updateTitle:ORKLocalizedString(@"MEMORY_GAME_TIMEOUT_TITLE", nil) text:ORKLocalizedString(@"MEMORY_GAME_TIMEOUT_MESSAGE", nil)];
_contentView.buttonItem = [ORKBorderedButton new];
[_contentView.buttonItem setTitle:ORKLocalizedString(@"BUTTON_NEXT", nil) forState:UIControlStateNormal];
[_contentView.buttonItem addTarget:self action:@selector(tryAgainAction) forControlEvents:UIControlEventTouchUpInside];
}
#pragma mark ORKSpatialSpanStepStateComplete
- (void)showComplete {
[self.activeStepView updateTitle:ORKLocalizedString(@"MEMORY_GAME_COMPLETE_TITLE", nil) text:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.75 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self->_contentView.buttonItem = [ORKBorderedButton new];
[self->_contentView.buttonItem setTitle:ORKLocalizedString(@"BUTTON_NEXT", nil) forState:UIControlStateNormal];
[self->_contentView.buttonItem addTarget:self action:@selector(continueAction) forControlEvents:UIControlEventTouchUpInside];
});
}
- (void)showCopyright {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:ORKLocalizedString(@"MEMORY_GAME_COPYRIGHT_TEXT", nil)
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:ORKLocalizedString(@"BUTTON_OK", nil)
style:UIAlertActionStyleDefault
handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark ORKSpatialSpanStepStateRestart
- (void)doRestart {
[self resetForNewGame];
dispatch_async(dispatch_get_main_queue(), ^{
// Dispatch, so we don't do this in the middle of a state transition
if (self->_state.state == ORKSpatialSpanStepStateRestart) {
[self transitionToState:ORKSpatialSpanStepStatePlayback];
}
});
}
- (void)showPausedFromState:(ORKState *)fromState {
[self updateGameRecordOnPause];
// Do not update game counters - doesn't count as a game.
[_activityTimer invalidate]; _activityTimer = nil;
[_playbackTimer invalidate]; _playbackTimer = nil;
[self resetForNewGame];
[self.activeStepView updateTitle:ORKLocalizedString(@"MEMORY_GAME_PAUSED_TITLE", nil) text:ORKLocalizedString(@"MEMORY_GAME_PAUSED_MESSAGE", nil)];
_contentView.buttonItem = [ORKBorderedButton new];
[_contentView.buttonItem setTitle:ORKLocalizedString(@"BUTTON_NEXT", nil) forState:UIControlStateNormal];
[_contentView.buttonItem addTarget:self action:@selector(continueAction) forControlEvents:UIControlEventTouchUpInside];
}
#pragma mark State machine
- (void)initializeStates {
NSMutableDictionary *states = [NSMutableDictionary dictionary];
states[@(ORKSpatialSpanStepStateInitial)] = [ORKState stateWithState:ORKSpatialSpanStepStateInitial
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this resetGameAndUI];
}
exitHandler:nil context:self];
states[@(ORKSpatialSpanStepStatePlayback)] = [ORKState stateWithState:ORKSpatialSpanStepStatePlayback
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this startPlayback];
}
exitHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this finishPlayback];
} context:self];
states[@(ORKSpatialSpanStepStateGameplay)] = [ORKState stateWithState:ORKSpatialSpanStepStateGameplay
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this startGameplay];
}
exitHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this finishGameplay];
} context:self];
states[@(ORKSpatialSpanStepStateSuccess)] = [ORKState stateWithState:ORKSpatialSpanStepStateSuccess
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this showSuccess];
}
exitHandler:nil context:self];
states[@(ORKSpatialSpanStepStateFailed)] = [ORKState stateWithState:ORKSpatialSpanStepStateFailed
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this showFailed];
}
exitHandler:nil context:self];
states[@(ORKSpatialSpanStepStateTimeout)] = [ORKState stateWithState:ORKSpatialSpanStepStateTimeout
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this showTimeout];
}
exitHandler:nil context:self];
states[@(ORKSpatialSpanStepStateRestart)] = [ORKState stateWithState:ORKSpatialSpanStepStateRestart
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this doRestart];
}
exitHandler:nil context:self];
states[@(ORKSpatialSpanStepStateComplete)] = [ORKState stateWithState:ORKSpatialSpanStepStateComplete
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this showComplete];
} exitHandler:nil context:self];
states[@(ORKSpatialSpanStepStateStopped)] = [ORKState stateWithState:ORKSpatialSpanStepStateStopped
entryHandler:nil
exitHandler:nil
context:self];
states[@(ORKSpatialSpanStepStatePaused)] = [ORKState stateWithState:ORKSpatialSpanStepStatePaused
entryHandler:^(ORKState *from, ORKState *to, ORKSpatialSpanMemoryStepViewController *this) {
[this showPausedFromState:from];
} exitHandler:nil context:self];
_states = states;
[self transitionToState:ORKSpatialSpanStepStateInitial];
}
- (void)transitionToState:(ORKSpatialSpanStepState)state {
ORKState *stateObject = _states[@(state)];
ORKState *oldState = _state;
if (oldState.exitHandler != nil) {
oldState.exitHandler(oldState, stateObject, oldState.context);
}
_state = stateObject;
if (stateObject.entryHandler) {
stateObject.entryHandler(oldState, stateObject, stateObject.context);
}
}
@end