I want to ask this question because I have been thinking about it for a while and I haven't discovered a solution that I am very happy with.
When coding games, I've frequently run into the situation where it seems advantageous to step out of the OOP mindset and into a sort of functional programming mindset for a brief moment in order to calculate the results of some action. This will usually happen at the Game level, and the results will be applied to any of the objects in the Game as necessary.
In the current situation I have implemented ability cards in my strategy game. The player discovers the cards as they out mine the floors. Once discovered they can be played at any time to gain a one time bonus. This bonus could be anything, so it could need access to the values of the Game such as resources and food, access to the values of the Towers such as the Floors and the Items and Enemies on them, or access to the workers contained in the Tower or in its Floors. Just to be clear, these are things like, "Gain 10 workers," or "Clean up all destroyed floors instantly," or "Pick up all items instantly," or "All workers gain X skill upgrades," or many other similar ideas depending on the balance of the game.
As a result, the logic for what to do is contained in the Game class itself. I don't know if this is bad practice or not. The Card objects simply choose a random type when instantiated, and the types are held in an enum. When a card is played at the Game level, it uses a switch based on the enum values to decide what logic to run.
DTAbilityCardType.h
typedef NS_ENUM(NSInteger, AbilityCardType){
AbilityCardTypeNone = 0,
AbilityCardTypeReduceCountdowns,
AbilityCardTypeDestroyAllEnemies,
AbilityCardTypePickupAllItems,
AbilityCardTypeSlowAging,
AbilityCardTypeGainMigrants
};
DTAbilityCard.h
#import <Foundation/Foundation.h>
#import "DTAbilityCardType.h"
@interface DTAbilityCard : NSObject
@property AbilityCardType type;
@end
DTAbilityCard.m
#import "DTAbilityCard.h"
@implementation DTAbilityCard
//this must be equal to the final number of valid types in the enum
static const int kNumCardTypes = 5;
-(id) init {
self = [super init];
if (self) {
//plus one so none of them are type 0
int randomNumber = arc4random_uniform(kNumCardTypes) + 1;
_type = randomNumber;
}
return self;
}
@end
Now inside DTGame.m
#pragma mark - Ability Cards
-(void) buildHandOfCards {
int startingHandSize = 10;
for (int i = 0; i < startingHandSize; i++) {
[_abilityCardHand addObject:[[DTAbilityCard alloc]init]];
}
}
-(void) addNewCardToHand {
//the type of card will be random
[_abilityCardHand addObject:[[DTAbilityCard alloc]init]];
}
-(void) playCardNumber:(int)number {
DTAbilityCard *card = [self.abilityCardHand objectAtIndex:number];
[self doActionForCardType:card.type];
[self.abilityCardHand removeObjectAtIndex:number];
}
-(void) doActionForCardType:(AbilityCardType)cardType {
NSLog(@"played card type = %i", cardType);
DTTower *activeTower = [_towerArray objectAtIndex:_currentTower];
switch (cardType) {
case AbilityCardTypeReduceCountdowns:
[self reduceCountdownsOnDwarves];
break;
case AbilityCardTypeDestroyAllEnemies:
[self destroyAllEnemies];
break;
case AbilityCardTypePickupAllItems:
[activeTower allItemInstantPickup];
break;
case AbilityCardTypeSlowAging:
[self slowAgingOnDwarves];
break;
case AbilityCardTypeGainMigrants:
[activeTower addMigrantsToTower:10]; //10 is the number of migrants added
break;
default:
break;
}
}
It seems simple enough and it is pretty self documenting. If I want to add more cards, I add the values to the enum, change the constant in the AbilityCard class to the correct number, add the case to the switch statement in the Game, and then add a method to handle the logic. The worst case scenario, I will have to add one additional method to the Tower class for convenience if it is awkward for the Game to do whatever action is needed.
Ideally for OOP, the actual logic would somehow be encapsulated in each Card object. The only way I can think to do that would be to have the Card object contain a string that was converted to a selector, and use that selector to run a method that will still be in the Game anyway. I can't decide if that is better or just more complicated for no readability gain.