5
\$\begingroup\$

I've been learning Objective C for a few days so to test my skills I went ahead and made a simple Tic Tac Toe game using the console. I already have some experience with languages such as C++, Java and Python so I'm familiar with most of the concepts in Obj C.

How it works

  1. The player chooses their token (x or o)
  2. The player puts in the x and y coordinates of their token piece
  3. The computer makes its move
  4. Repeat 2 and 3 until someone wins

#import <Foundation/Foundation.h>
#include <stdlib.h>


// Game class
@interface Game : NSObject{
    BOOL isPlayersTurn;
    // players tokens (x or o)
    char playerToken;
    char computerToken;
    // 2d array stores tile values
    char board[3][3];
}

@end


@implementation Game
    -(id)init{
        self = [super init];

        isPlayersTurn = YES;

        // choosing players token
        while(!(playerToken=='x' || playerToken=='o')){
            NSLog(@"Choose your token (must be either x or o)");
            scanf("%c",&playerToken);
        }
        // computer token is opposite of player token
        computerToken = playerToken=='x'?'o':'x';

        // set tiles of board to empty
        for(int i=0; i<3; i++){
            for(int j=0; j<3; j++){
                board[i][j] = ' ';
            }
        }

        return self;
    }

    -(BOOL)finished{
        // check if someone has won
        for(int i = 0; i < 3; i++){
            // checks columns and rows
            if(board[i][0]==board[i][1]&&board[i][1]==board[i][2]&&board[i][0]!=' '){
                return true;
            }
            if(board[0][i]==board[1][i]&&board[1][i]==board[2][i]&&board[0][i]!=' '){
                return true;
            }
            // check diagonals
            if(board[0][0]==board[1][1]&&board[1][1]==board[2][2]&&board[0][0]!=' '){
                return true;
            }
            if(board[2][0]==board[1][1]&&board[1][1]==board[0][2]&&board[2][0]!=' '){
                return true;
            }
        }

        return false;
    }

    -(void)play{
        while(![self finished]){
            // print the current board
            NSLog([self getBoard]);
            // get the moves
            [self makeMoves];
        }
        // print the finished game board
        NSLog([self getBoard]);

        // winner is person who just had their move
        NSLog([isPlayersTurn?@"Computer":@"Player" stringByAppendingString:@" wins!"]);
        [self reset];
    }

    -(void)makeMoves{
        int x;
        int y;
        if(isPlayersTurn){
            // check their coordinate is either 1,2 or 3
            while(x!=1&&x!=2&&x!=3){
                NSLog(@"Choose your x coordinate");
                scanf("%i",&x);
            }
            while(y!=1&&y!=2&&y!=3){
                NSLog(@"Choose your y coordinate");
                scanf("%i",&y);
            }
            board[(int)y-1][(int)x-1] = playerToken;
        }else{
            // loop through until random tile is chosen that isn't already taken
            while(board[(int)y-1][(int)x-1]!=' '){
                x = arc4random()%3;
                y = arc4random()%3;
            }
            board[(int)y-1][(int)x-1] = computerToken;
        }
        // change turns
        isPlayersTurn = !isPlayersTurn;

    }

    // board in text form will be returned
    -(NSString*)getBoard{
        NSString* boardText = @"\n";
        if(isPlayersTurn){
            boardText = [boardText stringByAppendingString:@"Player's turn "];
        }else{
            boardText = [boardText stringByAppendingString:@"Computer's turn "];
        }
        // sets x axis
        boardText = [boardText stringByAppendingString:@"\n  | 1 | 2 | 3 |"];

        // sets boardText layout and y axis
        for(int i=0; i<3; i++){
            boardText = [boardText stringByAppendingString:@"\n"];
            boardText = [boardText stringByAppendingString:[[NSNumber numberWithInt:i+1] stringValue]];
            boardText = [boardText stringByAppendingString:@" |"];
            for(int j=0; j<3; j++){
                boardText = [boardText stringByAppendingString:@" "];
                boardText = [boardText stringByAppendingString:[NSString stringWithFormat:@"%c", board[i][j]]];
                boardText = [boardText stringByAppendingString:@" |"];
            }
        }
        return boardText;
    }

   // resets game
    -(void)reset{
        // reset variables
        for(int i=0; i<3; i++){
            for(int j=0; j<3; j++){
                board[i][j] = ' ';
            }
        }
        isPlayersTurn = YES;

        [self play];
    }

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Game *game = [[Game alloc]init];
        [game play];
    }
    return 0;
}

Sample

2016-02-28 15:55:10.298 Tic Tac Toe[7003:378858] Choose your token (must be either x or o)
o
2016-02-28 15:55:25.133 Tic Tac Toe[7003:378858] 
Player's turn 
  | 1 | 2 | 3 |
1 |   |   |   |
2 |   |   |   |
3 |   |   |   |
2016-02-28 15:55:25.134 Tic Tac Toe[7003:378858] Choose your x coordinate
1
2016-02-28 15:55:28.980 Tic Tac Toe[7003:378858] Choose your y coordinate
1
2016-02-28 15:55:29.549 Tic Tac Toe[7003:378858] 
Computer's turn 
  | 1 | 2 | 3 |
1 | o |   |   |
2 |   |   |   |
3 |   |   |   |
2016-02-28 15:55:29.550 Tic Tac Toe[7003:378858] 
Player's turn 
  | 1 | 2 | 3 |
1 | o |   |   |
2 | x |   |   |
3 |   |   |   |
2016-02-28 15:55:29.550 Tic Tac Toe[7003:378858] Choose your x coordinate
2
2016-02-28 15:55:30.588 Tic Tac Toe[7003:378858] Choose your y coordinate
2
2016-02-28 15:55:31.028 Tic Tac Toe[7003:378858] 
Computer's turn 
  | 1 | 2 | 3 |
1 | o |   |   |
2 | x | o |   |
3 |   |   |   |
2016-02-28 15:55:31.029 Tic Tac Toe[7003:378858] 
Player's turn 
  | 1 | 2 | 3 |
1 | o |   | x |
2 | x | o |   |
3 |   |   |   |
2016-02-28 15:55:31.029 Tic Tac Toe[7003:378858] Choose your x coordinate
3
2016-02-28 15:55:33.956 Tic Tac Toe[7003:378858] Choose your y coordinate
3
2016-02-28 15:55:34.420 Tic Tac Toe[7003:378858] 
Computer's turn 
  | 1 | 2 | 3 |
1 | o |   | x |
2 | x | o |   |
3 |   |   | o |
2016-02-28 15:55:34.420 Tic Tac Toe[7003:378858] Player wins!
2016-02-28 15:55:34.421 Tic Tac Toe[7003:378858] 
Player's turn 
  | 1 | 2 | 3 |
1 |   |   |   |
2 |   |   |   |
3 |   |   |   |
2016-02-28 15:55:34.421 Tic Tac Toe[7003:378858] Choose your x coordinate

I would appreciate it if you could give me feedback on how to make this program shorter/easier. I would also appreciate some advice on proper use of pointers and strings.

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

#include <stdlib.h>

We should not need to do this. First of all, we generally always prefer #import in Objective-C. But second of all, we don't even need this header file. I copy & pasted your code into Xcode, deleted this line, and everything worked just fine.


Input Validation

Getting user input is hard. And it's something that users always struggle with. It's important that you test that your code works not just when you get the input you expect to get, but importantly, when you get input you aren't expecting to get.

For example, because I am illiterate, when I ran your code for the very first time, I didn't even read the "x" in "Choose your x coordinate", so I gave the input 1,1.

Your program did not like receiving 1,1 as input.

Console output showing the same "Choose your y coordinate" prompt repeated

It entered an infinite loop.

The problem is that your loops here:

while(x!=1&&x!=2&&x!=3){
    NSLog(@"Choose your x coordinate");
    scanf("%i",&x);
}
while(y!=1&&y!=2&&y!=3){
    NSLog(@"Choose your y coordinate");
    scanf("%i",&y);
}

are expecting the user to only ever enter an integer. If it doesn't get an integer, it tries again. And tries again, and tries again. Importantly, because, for example, a comma can't be read in as an integer "%i", the comma I entered can't be cleared out of the buffer.

But as I said, getting user input is hard. I'd make the case that it'd be worth while to develop a whole set of functions just for taking user input and getting specific types back.

The one for reading an integer from a user looks something like this:

BOOL cleanStdIn() {
    while (getchar()!='\n');
    return YES;
}

int getIntFromUserWithPrompt(NSString *prompt) {
    int input;
    char c;
    
    do {
        printf("%s", [NSString stringWithFormat:@"\n%@", prompt].UTF8String);
    } while ((scanf("%d%c", &input, &c) != 2 || c != '\n') && cleanStdIn());
    
    return input;
}

And using the function is a bit simpler as well:

while(x!=1&&x!=2&&x!=3){
    x = getIntFromUserWithPrompt(@"Choose your x coordinate: ");
}
while(y!=1&&y!=2&&y!=3){
    y = getIntFromUserWithPrompt(@"Choose your y coordinate: ");
}

Now we're a bit better at taking some user input:

Console display rejecting bad input

Of course, we'd want different functions for different input types.


Warnings shouldn't be ignored.

Screenshot of code with compiler warnings highlighted

There are three warnings in our code base. We shouldn't ignore warnings. Even if these are harmless (I'm not commenting on whether or not they're harmless), if we start ignoring warnings, if we start getting used to running our project with warnings, then we'll soon find ourselves ignoring warnings that aren't harmless.

And the fix here is quite simple. Just add @"%@", between the opening parenthesis and the opening bracket, and you're fine.

Although in the last case, I'd rewrite that one as this:

NSLog(@"%@ wins!", isPlayersTurn ? @"Computer" : @"Player");

A little Object-Orientation can go a real long way.

On the whole, this code feels like procedural C with Objective-C's funky square bracket syntax. It's at least good that you moved your code out of main, but we should focus on programming in a more object oriented manner.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the feedback, can you give me an example of how I could make this game more object oriented? \$\endgroup\$
    – Cleye
    Commented Feb 29, 2016 at 0:04
  • \$\begingroup\$ @Cleye I'd recommend putting the tic tac toe game on hold (don't trash it, come back to it later), and implementing a fraction class or a complex number class (as I recommend in the article in the last link) as an exercise in learning OOP. After that, return to your game. \$\endgroup\$
    – nhgrif
    Commented Feb 29, 2016 at 0:10
2
\$\begingroup\$

Some additional points:

In -(void)makeMoves, the initial values of

int x;
int y;

are undefined, and testing their values in all the three while loops in that method is undefined behavior. Let's consider the last loop as an example:

// loop through until random tile is chosen that isn't already taken
while(board[(int)y-1][(int)x-1]!=' '){
    x = arc4random()%3;
    y = arc4random()%3;
}
board[(int)y-1][(int)x-1] = computerToken;

The values of x and y are undefined when the loop is entered, so this can easily crash your program or give a nonsensical result.

You should use a do { } while () loop instead:

do {
    x = arc4random() % 3;
    y = arc4random() % 3;
} while (board[(int)y-1][(int)x-1] != ' ')
board[(int)y-1][(int)x-1] = computerToken;

Now x and y are initialized before being tested. The same applies to the other while-loops in that method.

But there is another error: The array indices in board[][] run from 0 to 2, and that is exactly what arc4random() % 3 returns. Subtracting one from x and y is wrong here! And the (int) cast is also not necessary:

do {
    x = arc4random() % 3;
    y = arc4random() % 3;
} while (board[y][x] != ' ')
board[y][x] = computerToken;

Also note that arc4random_uniform() is preferred over arc4random when creating random integers in a range, to avoid the so-called "modulo bias" (see e.g. Why do people say there is modulo bias when using a random number generator? or Arc4random modulo biased).

do {
    x = arc4random_uniform(3);
    y = arc4random_uniform(3);
} while (board[y][x] != ' ')
board[y][x] = computerToken;

This

// winner is person who just had their move
NSLog([isPlayersTurn?@"Computer":@"Player" stringByAppendingString:@" wins!"]);

is not correct, because the game can be a tie (nobody wins).


Finally: NSLog() prints a timestamp, process name and process id which each output. I would use a simple printf() instead to print to the console, e.g.

printf("Choose your x coordinate\n");
\$\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.