3
\$\begingroup\$

I implemented a version of the popular Tic Tac Toe game. It currently only supports two-player mode, but I'm planning to add artificial intelligence for user vs computer mode (which I don't know how, yet).

#include <stdio.h>
#include "cmdio.h" /* this defines getnum, yesno and select_menu */
#include <stdlib.h> /* exit */

typedef struct {
    int x;
    int y;
} BoardPosition;

typedef enum {
    NONE = 0,
    USER,
    COMP
} PlayerType;

typedef struct {
    PlayerType type;
    char symbol;
} Player;

typedef enum {
    HORIZONTAL = 0,
    VERTICAL,
    L_DIAGONAL,
    R_DIAGONAL
} WinMethod;

typedef struct {
    Player *player;
    WinMethod method;
} Winner;

typedef char Board[3][3];

/* interactive functions */
static void print_board(Board *);
static BoardPosition get_pos(Board *);

/* utility functions */
static void update_board(Board *, const BoardPosition, const Player *);
static Winner who_won(Board *, Player *, Player *);
static size_t count_board(Board *);
static void play_turn(Board *, Player *);

int main(void)
{
    Player player1 = { USER,  'X'};
    Player player2 = { USER, 'O' };
    /* The winner, for now. */
    Winner winner = { NULL /* none */,  0 /* whatever */};
    /* settle on configuration */
    puts("I want to play as ..");
    /* select_menu returns the index of the option the user chose */
    if (select_menu( (const char *[]) { "X", "O" }, 2, "> " )) {
        /* switch symbols */
        player1.symbol = 'O';
        player2.symbol = 'X';
    }
    puts("I want to play with ..");
    if (select_menu( (const char *[]) { "User", "Computer" }, 2, "> " ))
        player2.type = COMP;
    /* initializing the board */
    Board brd;
    for (int x = 0; x < 3; ++x)
        for (int y = 0; y < 3; ++y)
            brd[x][y] = 0;

    Player *player = &player1;
    for (;;) {
        /* print board */
        print_board(&brd);
        /* demand a position to fill */
        printf("Player %d's turn ..\n", (player == &player1) ? 1 : 2);
        play_turn(&brd, player);
        /* check for win */
        winner = who_won(&brd, &player1, &player2);
        if (winner.player == NULL) { /* no one won */
            /* check for draw */
            if (count_board(&brd) == 9) {
                puts("Draw!");
                return 0;
            }
            /* switch turns */
            player = (player == &player1) ? &player2 : &player1;
        } else if (winner.player == &player1) { /* player1 won */
            if (player1.type == COMP)
                puts("Oh! You lost .. Nevermind ... You'll win next time!");
            else
                puts("Congratulations! Player 1 won!");
            return 0;
        } else { /* player2 won */
            if (player2.type == COMP)
                puts("Oh! You lost .. Nevermind ... You'll win next time!");
            else
                puts("Congratulations! Player 2 won!");
            return 0;
        }
    }
    /* should never be reached */
    fputs("Internal Error!", stderr);
    return 1;
}

static Winner who_won(brd, player1, player2)
    Board *brd;
    Player *player1, *player2;
{
    int stat1 = 1;
    int stat2 = 1;

    // check horizontal, then vertical
    for (int i = 0; i < 2; ++i) {
        for (int x = 0; x < 3; ++x) {
            for (int y = 0; y < 3; ++y) {
                stat1 &= ((*brd)[i ? x : y][i ? y : x] == player1->symbol);
                stat2 &= ((*brd)[i ? x : y][i ? y : x] == player2->symbol);
            }
            if (stat1)
                return (Winner){player1, i};
            if (stat2)
                return (Winner){player2, i};
            stat1 = 1;
            stat2 = 1;
        }
    }

    // check left diagonal, then right diagonal
    for (int i = 0; i < 2; ++i) {
        for (int d = (i ? 0 : 2); i ? (d < 3) : (d >= 0); i ? (++d) : (--d)) {
            stat1 &= ((*brd)[d][d] == player1->symbol);
            stat2 &= ((*brd)[d][d] == player2->symbol);
        }
        if (stat1)
            return (Winner){player1, i + 2};
        if (stat2)
            return (Winner){player2, i + 2};

        stat1 = 1;
        stat2 = 1;
    }
    return (Winner){NULL, 0};
}

static void print_board(brd)
    Board *brd;
{
    char start = '1';
    for (int x = 0; x < 3; ++x) {
        for (int y = 0; y < 3; ++y) {
            if ((*brd)[x][y])
                printf("%c", (*brd)[x][y]);
            else
                printf("%c", start);
            if (y < 2)
                printf(" | ");
            else
                printf("\n");
            start += sizeof(char);
        }
    }
}

static BoardPosition get_pos(brd)
    Board *brd;
{
    BoardPosition pos;
    int p;
    while (1) {
        getnum(stdin, "Enter position: ", 1, 9, (long long *) &p);
        /* determine real position */
        int i = 0;
        for (pos.x = 0; pos.x < 3; ++pos.x)
            for (pos.y = 0; pos.y < 3; ++pos.y)
                if (++i == p && !(*brd)[pos.x][pos.y])
                    return pos;
        puts("Please provide an available position.");
    }
    return pos;
}

static void update_board(brd, pos, player)
    Board *brd;
    const BoardPosition pos;
    const Player *player;
{
    if (!player->type)
        return;
    (*brd)[pos.x][pos.y] = player->symbol;
}

static size_t count_board(brd)
    Board *brd;
{
    size_t count;
    for (int x = 0; x < 3; ++x)
        for (int y = 0; y < 3; ++y)
            count += !!(*brd)[x][y];
    return count;
}

static void play_turn(brd, player)
    Board *brd;
    Player *player;
{
    if (player->type == USER) {
        update_board(brd, get_pos(brd), player);
    } else {
        /* TODO: add artificial intelligence */
        fputs("Artificial Intelligence not yet implemented\n", stderr);
        exit(1);
    }
}

Well, what do you think?

\$\endgroup\$
  • \$\begingroup\$ Use the right tool for the right job. C is for: Low-level hardware control, Heavy number chrunching, Microprocesssors. Implementing Tic-Tac-Toe is a task you should do in a high level programming language. \$\endgroup\$ – Caridorc Dec 13 '14 at 14:42
  • \$\begingroup\$ @Caridorc: Actually, I wrote the above code just as a challenge for my proficiency in C. Specifically, the low level aspects of C such as character handling (notice bit manipulation ?). If you're suggesting higher level languages just for the sake of their simplicity and syntactic sugar, C is more familiar to me than Java, for instance. \$\endgroup\$ – Amr Ayman Dec 18 '14 at 16:55
3
\$\begingroup\$

I quite like it. It's got a reasonable structure and names.

I haven't see functions declared in this style:

static Winner who_won(brd, player1, player2)
    Board *brd;
    Player *player1, *player2;
{

in many a year. They've been obsolete for 25 years. You're very strongly discouraged from using them in new code.

Use the modern prototype-style instead:

static Winner who_won(Board *brd, Player *player1, Player *player2){

You've used enumerations which is good.

The line

 count += !!(*brd)[x][y];

is a little obscure. How about

count += ((*brd)[x][y])==NONE?0:1;

If you haven't encountered 'inline if' it's time to learn it.

\$\endgroup\$
  • \$\begingroup\$ "You're very strongly discouraged from using them in new code." What should be used instead? \$\endgroup\$ – confused00 Dec 15 '14 at 15:28
  • 1
    \$\begingroup\$ @confused00: static Winner who_won(Board *brd, Player *player1, Player *player2); As opposed the style in the OP where the types are declared after the ) and before the {. It was considered old-school when I learned 'C' in the late 80s. \$\endgroup\$ – user59064 Dec 15 '14 at 16:03
  • \$\begingroup\$ Concerning the old style, I actually think that style to be more readable and cleaner (clearer ?) than the newer style. As for !!(*brd)[x][y], I do know about 'inline Ifs' and I've used them a lot. For the specific purpose of this line, I think using double negation will work fine ... \$\endgroup\$ – Amr Ayman Dec 18 '14 at 17:03
  • \$\begingroup\$ Amr Ayman, The prototype style isn't considered a matter of style. The C committee declared it obsolete in 1989. The committee positively discourages compiler implementers and coders from supporting/using them. It's 25 years later.... PS: Their weakness is the parameter names appear twice so you have a maintenance overhead before you start. You can always lay out the prototype in a similar style. Double negative 'washing' will work fine. But is that really the most readable code? \$\endgroup\$ – user59064 Dec 18 '14 at 18:34
0
\$\begingroup\$

I think it looks very clean, very nice overall. Personal preference is the always bracketed approach, however. Even on single line if statements, I believe brackets should be used. I've seen people debug a code for an hour or more just because they should have had an enclosed bracket below their if statement, but instead just executed the first line.

 if (select_menu( (const char *[]) { "User", "Computer" }, 2, "> " ))
        player2.type = COMP;

to

 if (select_menu( (const char *[]) { "User", "Computer" }, 2, "> " )) {
        player2.type = COMP;
 }

I know it takes up an extra line, but to me the readability it just so much better.

\$\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.