Сапер на C


Мне было скучно в школе, и это было запрещено пользоваться телефонами на уроках; так что я начал писать игру сапер на мою книгу и испытал его на простой компилятор на моем телефоне и скопировать в мой компьютер после того, как я вернулся домой.

Я думаю, что мой код-это хорошо, но я боюсь, что я мог бы получать в эффект Даннинга-Крюгера.

#include <stdio.h>
#include <stdlib.h>
// seed functions
#include <time.h>
#include <string.h>
// timing functions
#include <sys/time.h>
// functions to catch signals
#include <signal.h>
#include <unistd.h>

// for the field itself
#define MINE 9
// for the mask
#define FLAG 9
#define EMPTY 0

#define IS_MINE(_Tile) (_Tile == MINE)

// the actual field
int **field;
// shows what the player knows
int **mask;
// sizes of the field
int height;
int width;
// mine count of the current field
int mine_cnt;

void help(char *prog, int h) {
    puts("Minestest Copyright (C) 2018 Arda Ünlü");
    puts("This program comes with ABSOLUTELY NO WARRANTY.");
    puts("This is free software, and you are welcome to redistribute it");
    puts("under certain conditions.");
    printf("Run \"%s --help\" for help.\n\n", prog);

    if(h) {
        printf("Usage: %s [difficulty | height width mine_count]\n", prog);
        puts("Difficulty can be easy, medium, or hard.");
        puts("Input format while playing:");
        puts("a b c");
        puts("a: x coordinate");
        puts("b: y coordinate");
        puts("c: 0 or 1: step on cell or flag cell (default: 0)");
    }
}

// handling signals and freeing malloc'd areas
// so we won't leak memory
// except a sigkill, then rip
void handle(int sig) {
    printf("Caught signal %d! Exiting.\n", sig);

    for(int i = 0; i < height; i++) {
        free(field[i]);
        free(mask[i]);
    }
    free(field);
    free(mask);

    exit(1);
}

// setup for the signal handler
// the actual handler is the function above, void handle(int sig)
void setupsig() {
    struct sigaction sigIntHandler;

    sigIntHandler.sa_handler = handle;
    sigemptyset(&sigIntHandler.sa_mask);
    sigIntHandler.sa_flags = 0;

    sigaction(SIGINT, &sigIntHandler, NULL);
}

void parse_args(int argc, char *argv[]) {
    if(argc > 1) {
        // predefined difficulty levels
        // and calling help
        if(argc == 2) {
            if((strcmp(argv[1], "-h") == 0)
            || (strcmp(argv[1], "--help") == 0)) {
                help(argv[0], 1);
                exit(0);
            }
            else if(strcmp(argv[1], "easy") == 0) {
                height   = 8;
                width    = 8;
                mine_cnt = 10;
            }
            else if(strcmp(argv[1], "medium") == 0) {
                height   = 16;
                width    = 16;
                mine_cnt = 40;
            }
            // "hard" lands here, as well as anything else
            else {
                height   = 16;
                width    = 30;
                mine_cnt = 99;
            }
        }
        // allow player to define their own difficulty levels
        else if(argc == 4) {
            height   = (atoi(argv[1]) ? : 16);
            width    = (atoi(argv[2]) ? : 30);
            mine_cnt = (atoi(argv[3]) ? : 99);
        }
        // arguments are invalid
        else {
            help(argv[0], 1);
            exit(1);
        }
    }
    // no arguments
    else {
        height   = 16;
        width    = 30;
        mine_cnt = 99;
    }

    // padding the field so that
    // we have zeroes all around
    height += 2;
    width  += 2;
}

void print_field() {
    char *cell[12] = {
        // cell labels, 0 to 8
        "\x1b[0m0",
        "\x1b[94m1",
        "\x1b[32m2",
        "\x1b[91m3",
        "\x1b[34m4",
        "\x1b[31m5",
        "\x1b[36m6",
        "\x1b[35m7",
        "\x1b[37m8",
        "\x1b[41;30mO\x1b[0m", //mine
        "\x1b[41;30mP\x1b[0m", //flag
    };

    printf("%d mine%s left.\n", mine_cnt, (mine_cnt == 1 ? "" : "s"));

    putchar(' '); // pad the x coordinates by 1 character

    // print x coordinates
    for(int x = 1; x < width-1; x++) {
        printf("%d", x%10);
    }
    putchar('\n');

    for(int y = 1; y < height-1; y++) {
        // reset color sequence and print the y coordinate
        printf("\x1b[0m%d", y%10);
        // print every cell on that y line
        for(int x = 1; x < width-1; x++) {
            // if the player knows anything about the cell
            // print it
            if(mask[y][x] != EMPTY) {
                printf("%s", mask[y][x] == FLAG ? cell[10] : cell[field[y][x]]);
            }
            // if they don't, just print a dot
            else {
                printf("\x1b[0m.");
            }
        }
        putchar('\n');
    }
    // reset colors after finishing printing
    printf("\x1b[0m\n");
}

int fill_field() {
    // be sure that there can be empty cells
    if(height * width < mine_cnt) {
        puts("Too many mines.");
        return 1;
    }

    // allocate our fields
    field = malloc(height * sizeof(int*));
    mask = malloc(height * sizeof(int*));

    // let's not segfault
    if(!field || !mask)
        return 1;

    // allocate every line
    for(int i = 0; i < height; i++) {
        field[i] = malloc(width * sizeof(int));
        mask[i] = malloc(width * sizeof(int));
        if(!field[i] || !mask[i])
            return 1;
    }

    // fill the mask
    for(int y = 0; y < height; y++) {
        for(int x = 0; x < width; x++) {
            mask[y][x] = !EMPTY;
        }
    }

#ifndef DEBUG
    for(int y = 1; y < height-1; y++) {
        for(int x = 1; x < width-1; x++) {
            mask[y][x] = EMPTY;
        }
    }
#endif

    // temporary variables for mine locations
    int coord_a, coord_b;

    // initialize main field
    for(int y = 0; y < height; y++) {
        for(int x = 0; x < width; x++) {
            field[y][x] = EMPTY;
        }
    }

    srand(time(NULL));

    //fill mines
    for(int i = 0; i < mine_cnt; i++) {
        coord_a = (rand() % (height-2)) + 1;
        coord_b = (rand() % (width-2))  + 1;
        //don't put mines in the same cell twice
        if(IS_MINE(field[coord_a][coord_b])) {
            i--;
            continue;
        }
        field[coord_a][coord_b] = MINE;
    }

    //fill numbers one by one
    for(int y = 1; y < height-1; y++) {
        for(int x = 1; x < width-1; x++) {
            // don't put a number if the current cell is a mine
            if(!IS_MINE(field[y][x])) {
                // looping the 3x3 adjacent cells
                for(int dy = -1; dy <= 1; dy++) {
                    for(int dx = -1; dx <= 1; dx++) {
                        // skip the current cell
                        if(dy == 0 && dx == 0) continue;
                        field[y][x] += IS_MINE(field[y + dy][x + dx]);
                    }
                }
            }
        }
    }

    return 0;
}

// this function has 2 uses that basically use the same algorithm.
// why == 1) if the cell player inputted was 0, extend that field
//           until we hit a cell which is adjacent to a mine
// why == 2) if the player inputs a cell which they already know,
//           assume they want to step on every adjacent cell
int explore_neighbors(int x, int y, int why) {
    // don't remove the flag, if there is one, if we are being
    // called as a recursion from the same function
    // if there is no flag, just remove the mask
    if(mask[y][x] != FLAG)
        mask[y][x] = !EMPTY;

    // counter for adjacent mines
    int cnt = 0;
    // loop the 3x3 adjacent cells
    for(int dy = -1; dy <= 1; dy++) {
        for(int dx = -1; dx <= 1; dx++) {
            // skip the current cell
            if(dy == 0 && dx == 0) continue;
            // why == 1) don't expand if there are mines in adjacent cells
            if(why == 1) {
                if(IS_MINE(field[y + dy][x + dx])) return 1;
            }
            // why == 2) count the mines in adjacent cells
            else
                cnt += IS_MINE(mask[y + dy][x + dx]);
        }
    }

    // if the user inputted a cell they certainly
    // know not to be empty
    if(why == 2 && cnt != 0) {
        for(int dy = -1; dy <= 1; dy++) {
            for(int dx = -1; dx <= 1; dx++) {
                // if there is a mine that was placed wrongly
                if(IS_MINE(mask[y + dy][x + dx])
                != IS_MINE(field[y + dy][x + dx])) {
                    // exit game
                    return 1;
                }
                // if not, do nothing
            }
        }
    }

    // why == 1) do this unconditionally
    // why == 2) if the number of adjacent mines correct
    if((why == 1) || (cnt == field[y][x])) {
        // loop the adjacent 3x3 block
        for(int dy = -1; dy <= 1; dy++) {
            for(int dx = -1; dx <= 1; dx++) {
                if(why == 1) {
                    // skip the current cell
                    if(dy == 0 && dx == 0) continue;
                    // expand if there are more empty areas
                    if(mask[y + dy][x + dx] == EMPTY)
                        explore_neighbors(x + dx, y + dy, 1);
                }
                // why == 2
                else {
                    // skip the current cell
                    if(dy == 0 && dx == 0) continue;
                    // unmask the adjacent cells
                    // while leaving the flags
                    if(mask[y + dy][x + dx] != FLAG)
                        mask[y + dy][x + dx] = !EMPTY;
                    // if there is a filed with no mines nearby,
                    // expand to there
                    if(field[y + dy][x + dx] == EMPTY)
                        explore_neighbors(x + dx, y + dy, 1);
                }
            }
        }
    }

    return 0;
}

int play_game() {
    int ret = 0;
    // internal variables for the loop
    int flag, finished, w;
    // input variables
    int in_x, in_y, in_f;

    char input_buf[99];

    print_field();

    while(1) {
        // prompt
        printf("> ");
        fgets(input_buf, 99, stdin);
        flag = sscanf(input_buf, "%d %d %d", &in_x, &in_y, &in_f);

        // if the user didn't enter a flag
        if(flag == 2) {
            //set the internal variable to 0
            in_f = 0;
            // increment the mine count variable if the
            // current cell was flagged previously
            if(IS_MINE(mask[in_y][in_x])) {
                mine_cnt++;
            }
        }
        // if the user entered an invalid input
        // just skip it
        else if(flag != 3) {
            continue;
        }
        puts("");

        // if the input is out of bounds, skip it
        if(in_x > width - 2 || in_y > height - 2 || in_x < 1 || in_y < 1) {
            continue;
        }

        // if the inputted cell is a mine
        // and the user didn't enter a flag
        // end game
        if(IS_MINE(field[in_y][in_x]) && in_f == 0) {
            ret = 1;
            break;
        }

        // if the current cell wasn't flagged before
        // and the user flagged now,
        // decrement the mine count variable
        if(in_f && !IS_MINE(mask[in_y][in_x])) mine_cnt--;

        // the reason to call explore_neighbors
        w = 0;
        // if the current cell is 0, we want to
        // expand into the zero-mine area
        if(field[in_y][in_x] == EMPTY) w = 1;
        // if the user know the cell they entered was empty,
        // then they thnk they know all the adjacent mines.
        // they want to expand to the adjacet 3x3 area.
        else if(mask[in_y][in_x] == !EMPTY) w = 2;
        // if the variable w is set and the user didn't enter a flag,
        // call explore_neighbors with the current cell coordinates
        // and the reason why we want to explore
        if(w && in_f == 0) {
            ret = explore_neighbors(in_x, in_y, w);
        }
        if(w != 2) ret = 0;
        // if ret is set, that means the user entered
        // a known cell with wrong adjacent mines.
        // exit game.
        else if(ret) break;

        // unmask or flag the current cell according to the input
        mask[in_y][in_x] = in_f ? FLAG : !EMPTY;

        // print the field after the operation
        print_field();

        finished = 0;
        for(int y = 0; y < height; y++) {
            for(int x = 0; x < width; x++) {
                // the variable starts as 0 and increments
                // only if the current cell is masked,
                // i.e. the user doesn't know about it yey
                finished += !mask[y][x];
            }
        }
        // if that variable is still zero that means the user
        // has unmasked/flagged every cell.
        // but we still have to check if they just randomly put
        // flags everywhere.
        if(!finished && mine_cnt == 0) {
            puts("Congratulations! You've beaten the game!");
            break;
        }
    }

    // if the user failed, control flow gets here
    if(ret == 1) {
        puts("Better luck next time!");

        // unmask ever cell and print
        for(int y = 0; y < height; y++) {
            for(int x = 0; x < width; x++) {
                mask[y][x] = !EMPTY;
            }
        }
        print_field();
    }

    // hopefully we won't leak any memory
    for(int i = 0; i < height; i++) {
        free(field[i]);
        free(mask[i]);
    }
    free(field);
    free(mask);

    return ret;
}

int main(int argc, char *argv[]) {
    // set up the signal handling code
    setupsig();

    int ret = 0;

    parse_args(argc, argv);

    help(argv[0], 0);

    // exit if an error occures while trying to set up the field
    if((ret = fill_field()) != 0) {
        return ret;
    }

    struct timeval begin, end;
    // the time when the game actually starts
    gettimeofday(&begin, NULL);
    ret = play_game();
    // the time after the game ends
    gettimeofday(&end, NULL);

    printf("Game lasted %.2f seconds.\n",
           (double) (end.tv_usec - begin.tv_usec) / 1000000
         + (double) (end.tv_sec  - begin.tv_sec));

    return ret;
}


1748
6
задан 20 февраля 2018 в 09:02 Источник Поделиться
Комментарии
2 ответа

Используйте соответствующие функции вместо макросов

Макросы можно сделать на удивление трудно справиться. Например IS_MINE(MINE ^ MINE) возвращает true, в то время как IS_MINE((MINE ^ MINE)) возвращает false.

Поскольку вы используете стандарте C99, вместо этого предпочитают встроенные функции:

inline int IS_MINE(int tile) { return tile == MINE; }

Предпочитаю нормальные (постоянные) переменные вместо макросов

static const int MINE = 9;
static const int FLAG = 9;
static const int EMPTY = 0;

Это важно как только вы измените int другой тип, как компилятор может теперь выдавать предупреждения.

Функция-специфическое только для чтения данных static и const

В cell в print_field никогда не изменяются, и только должен быть инициализированным раз. Нам не нужно колдовать Новый cell каждый раз.

Кроме того, мы должны убедиться, что мы не меняем содержимое cellпоэтому мы должны сделать это const:

static const char *cell[12] = {
....
};

Хранить глобальные переменные к минимуму

Да, это игра, но правильный struct что содержит ваше текущее состояние позволяет быть уверенным, что вы а) не случайно изменить глобальную переменную, где вы не намерены и Б) Не забудьте любой игры переменной.

Использование перечислений для переменных, которые содержат только некоторые определенные значения

Соседи могут сделать изучены в двух случаях, поэтому либо why = 1 или why = 2 в explore_neighbors.

Но это волшебные цифры. Мы можем перечислить эти причины и вместо того, чтобы использовать перечисление:

enum exploration {
EXPLORE_EMPTY_CELLS, //!< explores all surrounding non-mine cells
REVEAL_SURROUNDING_CELLS //!< explores all cells around the current cell
};

Это намного проще, чтобы прочитать позже в коде. Сравнить

explore_neighbors(x, y, EXPLORE_EMPTY_CELLS);

для

explore_neighbors(x, y, 1);

Используйте описательные имена переменных

i для итерации-это хорошо, но w "разведка почему жхй" нет.

Сохранить объем ваших переменных короткое

Вы уже используете стандарте C99, так держать сферу ваших переменных к минимуму. Вы никогда не используете flag за пределами вашего while(1) в play_game, чтобы переместить его в петлю, к примеру.

Использовать sizeof вместо магических чисел в массивах с статический размер

В play_gameвы используете 99 дважды:

char input_buf[99];

print_field();

while(1) {
// prompt
printf("> ");
fgets(input_buf, 99, stdin);

Это может привести к ошибкам. Вы можете изменить 99, сделать телефонный звонок или позовут на встречу, и тогда вы забыли изменить других. Вместо этого используйте sizeof или константа времени компиляции вместо:

print_field();

while(1) {
char input_buf[99];
// prompt
printf("> ");
fgets(input_buf, sizeof(input_buf), stdin);

Использовать sizeof(array)/sizeof(array[0]) если вы не используете char в схожей ситуации.

Предпочитает одиночную распределения для поля

Нам не нужно звонить malloc так часто. Ни одного звонка-это нормально, если мы получаем доступ к клеткам, как

field[x + y * width];

или похожие. Меньше ассигнований значит меньше возможных ошибок. Вы можете использовать

// If you keep `width' and `height' global, the function
// will be index_ptr(int x, int y, int * memory)

inline int* index_ptr(int width, int height, int x, int y, int * memory) {
assert(0 <= x && x < width);
assert(0 <= y && y < height);

return memory + x + (y * width);
}

inline int index(int width, int height, int x, int y, int * memory) {
return *index_ptr(width, height, x, y, memory);
}

если вы не хотите вспоминать формулы. Обратите внимание, что это имеет приятный побочный эффект, что теперь мы можем добавить дополнительные проверки для наших посетителей.

Наши выделения для field и mask получает теперь легко, а также:

field = malloc(sizeof(*field) * width * height);
mask = malloc(sizeof(*mask) * width * height);

8
ответ дан 21 февраля 2018 в 05:02 Источник Поделиться

Хорошие вещи

Впечатляет раннее использование обработки сигнала.

Хорошее форматирование.

Хороший уровень комментария.

Функциональные ошибки?

Что-то дело неладно, касающиеся height и width. Я ожидал height возможные значения coord_aне height-2.

// ??
coord_a = (rand() % (height-2)) + 1;

С помощью суб-диапазона height-2, height * width < mine_cnt потом не выглядеть правильный тест.

Я бы рекомендовала рассмотреть и терпеть минные поля, как малые, как 1х1.

Ах, теперь я вижу, height += 2; width += 2; которые выполняют большую часть этой проблемы.

Как height это уже не минное поле высоты, использовать другое имя переменной хотел бы добавить ясности. height_p2 может быть?

В любом случае, if(height * width < mine_cnt) скорее всего, неправильно. Возможно if((height - 2) * (width - 2) < mine_cnt)

Нестандартный код

Составляем сообщил: "ИСО с запрещает опуская средний срок ?: выражение".

// height   = (atoi(argv[1]) ? : 16);
// Perhaps?
int i = atoi(argv[1]);
height = i > 0 ? i : 16; // also perform sign check

// Later code depends on `height > 2` so maybe
height = i > 2 ? i : 16;
// Also suggest an sane upper bound check

Избежать голый магических чисел

Добавить комментарий к деталям экране последовательности или использовать некоторые константы

    "\x1b[94m1",  // Bright Blue
"\x1b[32m2", // Green
// or
#define GREEN "\x1b[32m2"

Пусть вычислить код

// fgets(input_buf, 99, stdin);
fgets(input_buf, sizeof input_buf, stdin);

функции malloc() стиль

А не ptr = malloc(n * sizeof (some_type))рассмотрим ptr = malloc(sizeof *ptr * n). Это проще, чтобы код правильно, пересматривать и поддерживать. Ведущий с sizeof() обеспечивает правильную математику для более сложных вычислений.

// field = malloc(height * sizeof(int*));
field = malloc(sizeof *field * height);

Идея отладки

Разрешить выбор семян, через параметры команды, для srand(time(NULL));. Затем можно использовать нужные настройки.


Мелкие вещи следует


Глобальные переменные

Я не являюсь ни поклонником, ни противником глобальных переменных. Любой из этих подходов есть достоинства, но в этом случае необходимо продумать структуру хранения данных головоломки и пройти b_mine *state С помощью функций. Такой подход позволяет для будущего роста и, по моему опыту, легче отлаживать.

// int **field;
//...
// int mine_cnt;

typedef struct {
int **field;
//...
int mine_cnt;
} b_mine;

Почему #определить?

Хотя #define IS_MINE(_Tile) (_Tile == MINE) безусловно доброкачественным и оперативное использование макроса для is_mine() функции, будьте осторожны с использованием define когда функция будет достаточно.

Сцепить литералы

Альтернативы много puts()

    puts(
"Input format while playing:\n"
"a b c\n"
"a: x coordinate\n";
...
"c: 0 or 1: step on cell or flag cell (default: 0)");

5
ответ дан 21 февраля 2018 в 05:02 Источник Поделиться