5
\$\begingroup\$

This code currently works, but I'm new to Objective-C from a Python/PHP background. How can I improve it/make it more Objective-Cesque?

Header File/Interface

#import <Foundation/Foundation.h>

@interface Monster : NSObject

@property NSString* name;
@property NSNumber* health;
@property NSNumber* strength;
@property NSNumber* dexterity;
@property NSNumber* magic;

//

- (id) initWithName: (NSString*) nameToBeConstructedWithMethodConstructorInitWithName;
- (void) attack: (Monster*) sentMonster blocking:(BOOL)block isWithMagic:(BOOL)magicYes andMagicType:(NSString*)whatMagic andBurn:(NSInteger)burning;
- (bool) isDead;
- (void) printInfo;


@end

Implementation File

#import "Monster.h"

@implementation Monster

- (id) initWithName: (NSString*) nameToBeConstructedWithMethodConstructorInitWithName{
    self = [super init];

    if (self){
        _name = nameToBeConstructedWithMethodConstructorInitWithName;
        _dexterity = @((arc4random() % 15) + 3);
        _strength = @((arc4random() % 15) + 3);
        _magic = @((arc4random() % 15) + 3);
        _health = @((arc4random() % 40) + 60);
    }
    return self;
}

- (void) attack: (Monster*) sentMonster blocking:(BOOL)block isWithMagic:(BOOL)magicYes andMagicType:(NSString*)whatMagic andBurn:(NSInteger)burning{
    NSInteger health = [sentMonster.health intValue];
    NSInteger modifier = [self.strength intValue];
    if (modifier > 5) modifier = 1;
    if (modifier > 10) modifier = 2;
    if (modifier > 15) modifier = 5;
    NSInteger hit = 0;

    if (magicYes){
        if ([whatMagic characterAtIndex:0] == 'i'){
            NSInteger baseHit = ((arc4random() % 15) + 5);
            NSLog(@"%@'s health: %ld", sentMonster.name, health);
            hit = baseHit + modifier;
            health -= hit;
            sentMonster.health = @(health);
            NSLog(@"%@ was hit for %ld magical damage and has a new health of %ld", sentMonster.name, hit, health);
        }
        if ([whatMagic characterAtIndex:0] == 'f'){
            NSInteger baseHit = ((arc4random() % 25) + 10);
            NSLog(@"%@'s health: %ld", sentMonster.name, health);
            hit = baseHit + modifier;
            if (burning) health -= ((arc4random()% 7) + 3);
            health -= hit;
            sentMonster.health = @(health);
            NSLog(@"%@ was hit for %ld magical damage and has a new health of %ld", sentMonster.name, hit, health);
        }
    }

    else if (((arc4random() % 20) + [sentMonster.dexterity intValue]) >= 11){
        NSInteger baseHit = ((arc4random() % 3)+2);
        NSLog(@"%@'s health: %ld", sentMonster.name, health);
        NSLog(@"Basehit: %ld",baseHit);
        NSLog(@"Modifier: %ld:",modifier);
        if (block) hit = baseHit;
        else hit = baseHit + modifier;
        NSLog(@"Hit: %ld", hit);
        health -= hit;
        sentMonster.health = @(health);
        NSLog(@"%@ was hit for %ld physical damage and has a new health of %ld", sentMonster.name, hit, health);

    }
    else {
        NSLog(@"%@ successfully dodged an attack", sentMonster.name);
    }
}

- (bool) isDead{
    if ([self.health intValue] <= 0)return YES;
    return NO;

}

- (void) printInfo{
    NSLog(@"\nName: %@\nHealth: %@\nStrength: %@\nDexterity: %@\nMagic: %@\n",self.name, self.health, self.strength, self.dexterity, self.magic);
}



@end

Main

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

NSString* getNSString(NSString *prompt);

int main()
{

    @autoreleasepool {
        Monster* player = [[Monster alloc]initWithName:getNSString(@"What is the player's name:\n")];
        Monster* monster = [[Monster alloc]initWithName:@"Brad"];

        BOOL whichMonsterWon,skipMonsterAttack,isCast;
        NSInteger chanceSkip, chanceBurn;
        NSString* magicType = getNSString(@"Do you use (I)ce or (F)ire magic");
        magicType = [magicType lowercaseString];
        NSLog(@"\n\n\n");

        [player printInfo];
        [monster printInfo];

        while (YES){
            chanceSkip = 0;
            skipMonsterAttack = NO;
            if ([monster isDead]){
                whichMonsterWon = YES;
                break;
            }
            if ([player isDead]){
                whichMonsterWon = NO;
                break;
            }
            NSString* playerChoice = getNSString(@"\n(A)ttack\n(B)lock\n(M)agic\n");
            playerChoice = [playerChoice lowercaseString];
            if ((isCast) && ([playerChoice characterAtIndex:0] == 'm')) NSLog(@"You have already cast your magic!");
            else if ([playerChoice characterAtIndex:0] == 'a') [player attack:monster blocking:NO isWithMagic:NO andMagicType:magicType andBurn:chanceBurn];
            else if ([playerChoice characterAtIndex:0] == 'b'){
                [monster attack:player blocking:YES isWithMagic:NO andMagicType:magicType andBurn:chanceBurn];
                skipMonsterAttack = YES;
            }
            else if (([playerChoice characterAtIndex:0] == 'm') && !isCast){
                if ([magicType characterAtIndex:0] == 'i'){
                    chanceSkip = arc4random() % 2;
                    if (chanceSkip) NSLog(@"%@ was stunned for one turn",monster.name);
                }
                if ([magicType characterAtIndex:0] == 'f'){
                    chanceBurn = arc4random() % 2;
                    if (chanceBurn) NSLog(@"%@ was burned for one turn",monster.name);
                }
                [player attack:monster blocking:NO isWithMagic:YES andMagicType:magicType andBurn:chanceBurn];
                chanceBurn = 0;
                isCast = YES;

            }
            else NSLog(@"You have entered an unknown command, %@ gets a free turn", monster.name);
            if (!(skipMonsterAttack) && !chanceSkip) [monster attack:player blocking:NO isWithMagic:NO andMagicType:magicType andBurn:chanceBurn];
        }
        [player printInfo];
        [monster printInfo];

        if (whichMonsterWon == YES) NSLog(@"%@ beat the monster!",player.name);
        if (whichMonsterWon == NO) NSLog(@"%@ beat the player!",monster.name);
    }
    return 0;
}

NSString* getNSString(NSString *prompt){
    NSLog(@"%@", prompt);
    char cString[100] = "";
    scanf("%s", cString);
    NSString* oString = [NSString stringWithUTF8String:cString];
    return oString;
}
\$\endgroup\$
7
\$\begingroup\$

First and foremost, the best way to make this "more Objective-Cesque" would be to develop a UI for it. Whether OSX or iOS, the tools available for develop UI are really quite good... but let me look at the rest of your code... ;)


I can't see any good reason for your Monster class properties (other than name) to be objects. Simple primitive data types would more than suffice here. It would make parts of your code simpler, more readable, and as far as I can tell, you don't actually need these values as NSNumber anywhere. Even if you do, it's extraordinarily simply to create the NSNumber when it's needed, if that ever comes around, so let's just use an int here?

But moreover, should these properties necessarily be public? And even if they're public readable, should they by public writable?

As far as I can tell, you don't need these variables publicly, so let's make these private. Keep the name property in the .h, but move the rest into a class category in the .m file:

Monster.m

#import Monster.h

@interface Monster()

@property int health;
@property int strength;
@property int dexterity;
@property int magic;

@end

@implementation Monster

...

There are aspects to - (void) attack: (Monster*) sentMonster blocking:(BOOL)block isWithMagic:(BOOL)magicYes andMagicType:(NSString*)whatMagic andBurn:(NSInteger)burning; that I find troubling from an Objective-C stand point and from an OOP standpoint.

First off all, I think the monster who stands to have properties changed is the one the method should be called on. Instead, the attacking monster is the monster on which the method is called and none of his stats are modified.

The method should either be rewritten to be called on the defending monster or possibly be rewritten into a class method which is called on neither, but rather both are sent as arguments.

But I also think we can probably have a class for our magic types. Or maybe our attack types. Right now, isWithMagic:, andMagicType:, and andBurn: are only relevant if you're using magic. What about an attack-type class? Objects would hold information on what type of magic (or non-magic) the attack was and whether or not it burned.

And finally, we can add a isBlocking property to the Monster class which is just an on/off bool for whether the monster is currently blocking. So now, the header for the method will look more like this:

- (void)attackedBy:(AttackType*)incomingAttack;

And instead of sentMonster.health, we'll be referring to self.health. And the incomingAttack variable will have properties describing the magic type (or non magic), how much damage it might do, whether or not it burns and for how much, etc.


if ((isCast) && ([playerChoice characterAtIndex:0] == 'm')) NSLog(@"You have already cast your magic!");

// stuff

else if (([playerChoice characterAtIndex:0] == 'm') && !isCast)

This section of code is bothersome to me. Particularly because there are two other if statements in the middle. Even if there weren't though, this is still bothersome.

I'd simply take the isCast out of both of these and refactor as such:

if ([playerChoice characterAtIndex:0] == 'm') {
    if (isCast) {
        // stuff
    } else {
        // stuff
    }
} else if ([playerChoice...

This is better for readability in a couple of ways. First of all, all the if statements in this tree correspond to different user selections, and within this if we handle what happens EVERY TIME the user makes that selection, and second, because on top of the statement I just made, Xcode will let me collapse this entire if ... == 'm') section all at once if I don't need to look at it any more.


But with that said...

NSString* getNSString(NSString *prompt)

First of all, this is poorly named. It's not a getter (even though in English, its functionality could easily be described as getting something). And its description of what it is getting isn't helpful at all. We KNOW it returns a NSString regardless of what it's named.

But why do we bother turning this into a string and then once we have that string, we use an NSString method to pull the first character out of that string for comparison? Can we change that method to this:

char fetchUserInput(NSString *prompt)

And within this function, include the logic for simply returning the first character the use entered?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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