Конечный Tic-Tac-Toe в C

Вот моя попытка в UTTT (в ответ на Перезагрузка выходных дней ). Вот что я хотел бы критиковать:

  • Я несколько раз тестировал код для ошибок, но, возможно, я пропустил некоторые из них.

  • Я чувствую, что у меня есть дублированный код в некоторых местах (с небольшими изменениями, являющимися разницей), уточняя их немного, было бы хорошо.

  • Улучшение синтаксического анализа ввода

Но любые и все предложения приемлемы. Если вам интересно посмотреть на некоторые обновленные версии этого кода, посмотрите репозиторий Github, в котором размещается код (не стесняйтесь отправлять вилку и отправлять запросы на тягу).

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define ROWS 9
#define COLS 9

typedef char Board[ROWS][COLS];
typedef char MetaBoard[ROWS / 3][COLS / 3];
typedef enum {VALID, NOT_A_DIGIT, NOT_IN_BOARD, SPACE_OCCUPIED, OUT_OF_BOUNDS} MoveStatus;

void fillSubBoard(Board board, int x, int y, char c)
{
    for (; (x % 3) != 0; x--); // quickly set x to left bound of sub-board
    for (; (y % 3) != 0; y--); // quickly set y to upper bound of sub-board
    for (int rowMax = x + 2, row = x; row <= rowMax; row++)
    {
        for (int columnMax = y + 2, column = y; column <= columnMax; column++)
        {
            board[row][column] = c;
        }
    }
}

int getRowBound(int row)
{
    switch (row)
    {
        case 0 ... 2:
            return 0;
        case 3 ... 5:
            return 1;
        case 6 ... 8:
            return 2;
        default:
            return -1;
    }
}

int getColumnBound(int column)
{
    switch (column)
    {
        case 0 ... 2:
            return 0;
        case 3 ... 5:
            return 1;
        case 6 ... 8:
            return 2;
        default:
            return -1;
    }
}

void printBoard(Board board)
{
    printf("\n=============||===========||=============\n");
    for (int row = 0; row < ROWS; row++)
    {
        printf("||");
        for (int column = 0; column < COLS; column++)
        {
            if (board[row][column] == '-') printf("%d,%d|", row, column);
            else printf(" %c |", board[row][column]);
            if (0 == (column+1) % 3) printf("|");
        }
        if ((row+1) % 3 == 0) printf("\n=============||===========||=============\n");
        else printf("\n-----|---|---||---|---|---||---|---|-----\n");
    }
}

static int checkMeta(MetaBoard meta)
{
    const int xStart[ROWS - 1] = {0,  0,  0,  0,  1,  2,  0,  0};
    const int yStart[COLS - 1] = {0,  1,  2,  0,  0,  0,  0,  2};
    const int xDelta[ROWS - 1] = {1,  1,  1,  0,  0,  0,  1,  1};
    const int yDelta[COLS - 1] = {0,  0,  0,  1,  1,  1,  1,  1};
    static int startx, starty, deltax, deltay;
    for (int trip = 0; trip < ROWS - 1; trip++)
    {
        startx = xStart[trip];
        starty = yStart[trip];
        deltax = xDelta[trip];
        deltay = yDelta[trip];
        // main logic to check if a subboard has a winner
        if (meta[startx][starty] != '-' &&
            meta[startx][starty] == meta[startx + deltax][starty + deltay] &&
            meta[startx][starty] == meta[startx + deltax + deltax][starty + deltay + deltay]) return 1;
    }
    return 0;
}

static int checkBoard(Board board, MetaBoard meta, int player, int row, int column)
{
    const int xStart[ROWS - 1] = {0,  0,  0,  0,  1,  2,  0,  0};
    const int yStart[COLS - 1] = {0,  1,  2,  0,  0,  0,  0,  2};
    const int xDelta[ROWS - 1] = {1,  1,  1,  0,  0,  0,  1,  1};
    const int yDelta[COLS - 1] = {0,  0,  0,  1,  1,  1,  1,  1};
    static int startx, starty, deltax, deltay, status = 0;

    for (; (row % 3) != 0; row--); // quickly set row to left bound of sub-board
    for (; (column % 3) != 0; column--); // quickly set column to upper bound of sub-board

    for (int trip = 0; trip < ROWS - 1; trip++)
    {

        startx = row + xStart[trip];
        starty = column + yStart[trip];
        deltax = xDelta[trip];
        deltay = yDelta[trip];
        if (board[startx][starty] != '-' &&
            board[startx][starty] == board[startx + deltax][starty + deltay] &&
            board[startx][starty] == board[startx + deltax + deltax][starty + deltay + deltay])
        {
            fillSubBoard(board, row, column, (player == 1) ? 'X' : 'O');
            meta[getRowBound(row)][getColumnBound(column)] = (player == 1) ? 'X' : 'O';
            status = 1;
        }
    }
    return (status + checkMeta(meta)); // always check if the game has a winner
}

MoveStatus validCoords(Board board, int row, int column, int rowBound, int columnBound)
{
    if (!isdigit((char)(((int)'0') + row)) && !isdigit((char)(((int)'0') + column))) return NOT_A_DIGIT; // supplied coordinates aren't digits 1-9
    else if (row > ROWS - 1 || column > COLS - 1) return NOT_IN_BOARD; // supplied coordinates aren't within the bounds of the board
    else if (board[row][column] != '-') return SPACE_OCCUPIED; // supplied coordinates are occupied by another character
    else if (rowBound == -1 && columnBound == -1) return VALID; // supplied coordinates can move anywhere
    else if (((row > rowBound * 3 + 2 || column > columnBound * 3 + 2) ||
              (row < rowBound * 3 || column < columnBound * 3)) &&
             (rowBound > 0 && columnBound > 0)) return OUT_OF_BOUNDS; // coordinates aren't within the sub-board specified by the previous move
    else return VALID; // didn't fail anywhere else, so coords are valid
}

int main(void)
{
    int winner = 0, row = 0, column = 0, rowBound = -1, columnBound = -1, invalid = 0;
    char tempRow = '\0', tempColumn = '\0';
    Board board;
    MetaBoard meta;
    // initialize boards and fill with '-'
    memset(board, '-', ROWS * COLS * sizeof(char));
    memset(meta, '-', (ROWS / 3) * (COLS / 3) * sizeof(char));

    // game loop
    for (int turn = 0; turn < ROWS * COLS && !winner; turn++)
    {
        int player = (turn % 2) + 1;
        printBoard(board);
        printf("Player %d, enter the coordinates (x, y) to place %c: ", player, (player==1) ? 'X' : 'O');
        do
        {
            scanf("%c, %c", &tempRow, &tempColumn);
            for(; getchar() != '\n'; getchar()); // pick up superfluous input so we don't run into problems when we scan for input again
            row = abs((int) tempRow - '0');
            column = abs((int) tempColumn - '0');
            invalid = 0;
            switch (validCoords(board, row, column, rowBound, columnBound))
            {
                case NOT_A_DIGIT:
                    printf("Invalid input.  Re-enter: ");
                    invalid = 1;
                    break;
                case NOT_IN_BOARD:
                    printf("Out of board's bounds. Re-enter: ");
                    invalid = 2;
                    break;
                case SPACE_OCCUPIED:
                    printf("There is already an %c there.  Re-enter: ", board[row][column]);
                    invalid = 3;
                    break;
                case OUT_OF_BOUNDS:
                    printf("Your move was in the wrong sub-board.  Re-enter: ");
                    invalid = 4;
                    break;
                default:
                    break;
            }
        } while (invalid);

        board[row][column] = (player == 1) ? 'X' : 'O';
        switch(checkBoard(board, meta, player, row, column))
        {
            case 1:
                // next move can be anywhere
                rowBound = -1;
                columnBound = -1;
                break;
            case 2:
                winner = player;
                break;
            default:
                rowBound = row % 3;
                columnBound = column % 3;
                break;
        }
    }
    printBoard(board);

    if(!winner) printf("The game is a draw\n");
    else printf("Player %d has won\n", winner);

    return 0;
}
37 голосов | спросил syb0rg 5 MaramWed, 05 Mar 2014 03:59:34 +04002014-03-05T03:59:34+04:0003 2014, 03:59:34

4 ответа


24

Несколько комментариев:

[...]

for (; (x % 3) != 0; x--); // quickly set x to left bound of sub-board
for (; (y % 3) != 0; y--); // quickly set y to upper bound of sub-board

Я думаю, что я переместил бы код в раунд до нескольких из трех в свою собственную функцию. Я думаю, что я бы выполнил это примерно так:

int round3(int in) { return (in/3)*3; }

[...]

int getRowBound(int row)
{
    switch (row)
    {
        case 0 ... 2:
            return 0;
        case 3 ... 5:
            return 1;
        case 6 ... 8:
            return 2;
        default:
            return -1;
    }
}

int getColumnBound(int column)
{
    switch (column)
    {
        case 0 ... 2:
            return 0;
        case 3 ... 5:
            return 1;
        case 6 ... 8:
            return 2;
        default:
            return -1;
    }
}

Эти две функции (getRowBound и getColumnBound) идентичны - и не просто по совпадению, поэтому я думаю, что объединил бы их в одну функцию:

int getBound(int in) { 
    return (unsigned)in < 9 ? in / 3 : -1;
}

[...]

for (; (row % 3) != 0; row--); // quickly set row to left bound of sub-board
for (; (column % 3) != 0; column--); // quickly set column to upper bound of sub-board

Это должны быть вызовы в round3 (или любое другое имя, которое вы предпочитаете), упомянутое ранее.

[...]

Хотя некоторые не согласны (в некоторых случаях яростно), я лично предпочел бы избавиться от некоторых из таких условностей:

        fillSubBoard(board, row, column, (player == 1) ? 'X' : 'O');
        meta[getRowBound(row)][getColumnBound(column)] = (player == 1) ? 'X' : 'O';

... и вместо этого имеет что-то вроде:

static const char marks[] = {'X', 'O'};

// ...
fillSubBoard(board, row, column, marks[player]);
meta[getBound(row)][getBound(column)] = marks[player];

[...]

MoveStatus validCoords(Board board, int row, int column, int rowBound, int columnBound)
{
    if (!isdigit((char)(((int)'0') + row)) && !isdigit((char)(((int)'0') + column))) return NOT_A_DIGIT; // supplied coordinates aren't digits 1-9

Любой пользовательский ввод, который вы передаете в isdigit (или любой из других isXXX функций /макросов из ctype.h), должен быть добавлен сначала unsigned char. Передача отрицательного числа (кроме EOF) на isXXX дает неопределенное поведение. В типичном случае любой символ вне базового набора US-ASCII (например, любая буква, которая не используется на английском языке, плюс что-либо с диакритической меткой) будет иметь отрицательное значение при сохранении в char.

[...]

int winner = 0, row = 0, column = 0, rowBound = -1, columnBound = -1, invalid = 0;

Хотя some не согласен, я думаю, что most программисты предпочли бы каждую переменную в отдельном определении. Если (по какой-то причине) вы предпочитаете не делать этого, я бы, по крайней мере, отформатировал каждую переменную на отдельной строке.

        for(; getchar() != '\n'; getchar()); // pick up superfluous input so we don't run into problems when we scan for input again

Это выглядит багги. В состоянии, которое вы вызываете getchar() и проверяете возвращаемое значение, но затем в части цикла increment, вы вызываете getchar() снова, не проверяя возвращаемое значение.

Я думаю, вы, вероятно, хотите что-то большее:

while (getchar() != '\n')
    ;

[...]

        switch (validCoords(board, row, column, rowBound, columnBound))
        {
            case NOT_A_DIGIT:
                printf("Invalid input.  Re-enter: ");
                invalid = 1;
                break;
            case NOT_IN_BOARD:
                printf("Out of board's bounds. Re-enter: ");
                invalid = 2;
                break;
            case SPACE_OCCUPIED:
                printf("There is already an %c there.  Re-enter: ", board[row][column]);
                invalid = 3;
                break;
            case OUT_OF_BOUNDS:
                printf("Your move was in the wrong sub-board.  Re-enter: ");
                invalid = 4;
                break;

Здесь (опять) Я думаю, что я, вероятно, использовал бы возвращаемое значение для индексации в массив:

static char const *errors[] = {
    "Invalid Input.",
    "Out of board's bounds",
    "That space is already used",
    "Your move was in the wrong sub-board"
};

int error;
while (0 != (error=validCoords(...))) {
    printf("%s Re-enter:", errors[error]);
    getinput();
}

Это требует, чтобы вы сохраняли список ошибок в синхронизации с номерами ошибок, но выигрыш перевешивает дополнительное бремя(ИМО).

[...]

    board[row][column] = (player == 1) ? 'X' : 'O';

Опять же, я бы использовал ранее отмеченные marks, поэтому это закончилось бы так:

board[row][column] = marks[player];
ответил Jerry Coffin 5 MaramWed, 05 Mar 2014 08:46:20 +04002014-03-05T08:46:20+04:0008 2014, 08:46:20
11

Одно быстрое наблюдение, которое у меня есть, - это то, как вы храните игровые пространства. В то время как сетка 9 на 9 - это простой способ сделать это, это не самый ясный способ хранения и заканчивается тем, что требуется сложная логика адресации для получения каждой доски.

Ваши варианты работы с ним немного ограничены C, но есть поддержка типов вложенности. Вы можете использовать это, чтобы определить тип MainBoard, который использует матрицу 3-х и 3-х типов SubBoard и тип SubBoard, который занимает 3/3 сетки воспроизводимых квадратов (символов).

typedef char SubBoard[ROWS][COLS];
typedef SubBoard MainBoard[ROWS][COLS];

Я не уверен на 100% в синтаксисе, так как мой C немного ржавый, но это должно позволить вам использовать гораздо более простую адресацию. Если игрок использует пространство 1,3 на суб-доске, которую они играют, вы можете получить доску 1,3 для следующей игры.

ответил AJ Henderson 5 MaramWed, 05 Mar 2014 09:06:35 +04002014-03-05T09:06:35+04:0009 2014, 09:06:35
6
for (; (row % 3) != 0; row--); // quickly set row to left bound of sub-board
for (; (column % 3) != 0; column--); // quickly set column to upper bound of sub-board

Почему не просто

row -= row % 3;
column -= column % 3;

И это должно быть на самом деле вызывать что-то вроде

static inline int round3(int x) {
    return x - x % 3;
}
ответил aragaer 5 MarpmWed, 05 Mar 2014 13:59:45 +04002014-03-05T13:59:45+04:0001 2014, 13:59:45
6

В этом фрагменте кода:

int getRowBound(int row)
{
    switch (row)
    {
        case 0 ... 2:
            return 0;
        case 3 ... 5:
            return 1;
        case 6 ... 8:
            return 2;
        default:
            return -1;
    }
}

Эллипсис ... является расширением GCC для языка программирования C, известного как Диапазоны случаев . Использование этого препятствует переносимости, поскольку не все компиляторы поддерживают его (как и для большинства расширений). Если вы хотите избавиться от него, вы должны использовать функцию getBound, предложенную Джерри Коффином:

int getBound(int in) { 
    return (unsigned)in < 9 ? in / 3 : -1;
}
ответил Morwenn 6 MaramThu, 06 Mar 2014 03:19:40 +04002014-03-06T03:19:40+04:0003 2014, 03:19:40

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132