2D лабиринт с монстрами

Вступление


Я начал немного изучать программирование на C и хотел создать простую 2D консольную игру. Позвольте мне сначала познакомить вас с уровнем игры / структурой карты:

(1)  #################      (2)  #################
     #               #           #               #
     #         v  S ^#           #      S        #
     #               #           #               #
     #  <      #######           #       v       #
     #      >  #                 #      > <      #
     #    A    #                 #       ^       #
     #         #                 #              A#
     ###########                 #      A        #
                                 #               #
                                 #################

Различные символы представляют следующие игровые объекты:

  • # = стена
  • S = игрок
  • A = цель
  • ^,v,<,> монстр смотрит вверх / вниз / влево / вправо соответственно.

Цель состоит в том, чтобы добраться до одной из ячеек ворот, не касаясь монстра.

Может быть только один игрок S, но несколько монстров и целей на карте. На каждом тике игры игрок вводит w, a, s, или d и перемещает вверх / влево / вниз / вправо на одну ячейку соответственно. Затем все монстры перемещаются на одну единицу в соответствующем направлении.

Если игрок перемещается «в» стену, позиция игрока не обновляется. Однако монстры приходят в норму 180 deg от стены.

Цели (A) и стены (#) никогда не двигайся. Тем не мение, # действует как граница как для игрока, так и для монстров. Игрок может перейти на A но монстры также относятся к клеткам ворот как к стенам.

Одно предостережение заключается в том, что монстры могут перекрываться (см. Уровень 2), так что только монстр, впервые прочитанный из файла уровня, отображается в такой перекрывающейся ячейке. Если игрок сталкивается с монстром, он отображается сверху, и игра заканчивается распечаткой сообщения о проигрыше. Если игрок достигает цели, символ цели остается наверху, и игра заканчивается распечаткой сообщения о выигрыше.

Внутри я читаю файл уровня в 2D char array и сохраните все динамические сущности (игрока и монстров) в отдельной структуре данных. Затем я удаляю все динамические объекты из 2D-массива, чтобы использовать его в качестве «холста» для рисования. Таким образом, я могу обновить расположение всех сущностей, а затем решить, как они будут нарисованы на холсте, но при этом я все еще могу использовать статические элементы (# а также A) для обнаружения столкновений.

Код


Обратите внимание, что мне разрешено использовать только Стандарт C99.

common.h

#ifndef COMMON_H
#define COMMON_H

#include <stdio.h>

typedef enum error_code {
    OK = 0,
    COULD_NOT_OPEN_FILE = 1,
    COULD_NOT_READ_FILE = 2,
    INVALID_OPTIONS = 3,
    ALLOC_FAILED = 4
} t_error_code;

typedef struct error_object {
    char msg[100];
    t_error_code error_code;
} t_error_object;

t_error_object make_error(const char *message, t_error_code error_code);
int get_file_size(FILE *f);
int get_line_count(FILE *f);
char* strdup_(const char* src);

#endif

common.c

#include "common.h"

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

t_error_object make_error(const char *message, t_error_code error_code) {
    t_error_object error_obj;

    strncpy(error_obj.msg, message, 100);
    error_obj.error_code = error_code;

    return error_obj;
}

int get_file_size(FILE *f) {
    fseek(f, 0, SEEK_END);
    int len = ftell(f);
    fseek(f, 0, SEEK_SET);

    return len;
}

char *strdup_(const char *src) {
    char *dst = malloc(strlen (src) + 1);  
    if (dst == NULL) return NULL;          
    strcpy(dst, src);                      
    return dst;                            
}

game_params.h

#ifndef GAME_PARAMS_H
#define GAME_PARAMS_H

#include <stdio.h>
#include "common.h"

typedef struct game_params {
    FILE *level_file;

    FILE *input_file;
    FILE *output_file;
} t_game_params;


void cleanup_game_files(t_game_params *params);

t_error_object open_game_files(t_game_params *params,
                     const char* level_file_name,
                     const char* input_file_name,
                     const char* output_file_name);

t_error_object parse_game_parameters(t_game_params *params_out, int argc, char **argv);

#endif

game_params.c

#include "game_params.h"
#include "common.h"

#include <getopt.h>
#include <errno.h>
#include <string.h>

void cleanup_game_files(t_game_params *params) {
    if (params->level_file != NULL)
        fclose(params->level_file);

    params->level_file = NULL;

    if (params->input_file != NULL)
        fclose(params->input_file);

    params->input_file = NULL;

    if (params->output_file != NULL)
        fclose(params->output_file);

    params->output_file = NULL;
}

t_error_object open_game_files(t_game_params *params,
                     const char *level_file_name,
                     const char *input_file_name,
                     const char *output_file_name) {
    params->input_file = stdin;
    params->output_file = stdout;

    if (input_file_name != NULL) {
        if (strstr(input_file_name, ".txt") == NULL) {
            return make_error("Eingabe-Datei kann nicht gelesen werden", COULD_NOT_READ_FILE);
        }

        params->input_file = fopen(input_file_name, "r");
        if (params->input_file == NULL) {
            return make_error("Eingabe-Datei konnte nicht geöffnet werden", COULD_NOT_OPEN_FILE);
        }
    }

    if (output_file_name != NULL) {
        params->output_file = fopen(output_file_name, "w");
        if (params->output_file == NULL) {
            return make_error("Ausgabe-Datei konnte nicht geöffnet werden", COULD_NOT_OPEN_FILE);
        }
    }
 
    if (level_file_name == NULL) {
        return make_error("Level-Datei muss angegeben werden", COULD_NOT_OPEN_FILE);
    }

    params->level_file = fopen(level_file_name, "r");

    if (params->level_file == NULL) {
        return make_error("Level-Datei konnte nicht geöffnet werden", COULD_NOT_OPEN_FILE);
    }

    return make_error("", OK);
}

t_error_object parse_game_parameters(t_game_params *params_out, int argc, char **argv) {
    char *level_file_name = NULL;
    char *input_file_name = NULL;
    char *output_file_name = NULL;

    while (optind < argc) {
        if (argv[optind][0] != '-') {
            if (level_file_name != NULL)
                return make_error("Level-Datei darf nur einmal angegeben werden", INVALID_OPTIONS);

            level_file_name = argv[optind];
            optind++;
        }
    
        int opt;
        if ((opt = getopt(argc, argv, "i:o:")) != -1) {
            switch (opt) {
                case 'i':
                    if (input_file_name != NULL)
                        return make_error("Eingabe-Datei darf nur einmal angegeben werden", INVALID_OPTIONS);
                    input_file_name = optarg;
                    break;
                case 'o':
                    if (output_file_name != NULL)
                        return make_error("Ausgabe-Datei darf nur einmal angegeben werden", INVALID_OPTIONS);
                    output_file_name = optarg;
                    break;
                default:
                    return make_error("Falsche Optionen übergeben", INVALID_OPTIONS);
            }
        }
    }

    // Öffne die Dateien zum Lesen/Schreiben und speichere File-Handles in `params`
    t_error_object ret = open_game_files(params_out, level_file_name, input_file_name, output_file_name);
    return ret;
}

entity.h

#ifndef ENTITY_H
#define ENTITY_H

#include "board.h"

typedef enum entity_type {
    PLAYER,
    MONSTER,
    NO_ENT
} t_entity_type;

typedef struct position {
    int x;
    int y;
} t_position;

typedef struct entity {
    t_position pos;
    t_direction facing_dir;
    t_entity_type type;
} t_entity;

t_entity_type get_entity_type(char c);
t_entity create_entity(t_entity_type type, int x, int y, t_direction dir);

int compare_positions(t_position *pos1, t_position* pos2);

void handle_collision(t_board *board, t_entity *entity, t_position *new_pos);
int check_wall(t_board *board, t_position *new_pos);
int check_valid_move(t_board *board, t_position *new_pos);
void move_entity(t_board *board, t_entity *entity, t_direction dir);

#endif

entity.c

#include "common.h"
#include "direction.h"
#include "entity.h"
#include "board.h"

t_entity_type get_entity_type(char c) {
    int is_player = (c == 'S');
    int is_monster = (map_char_to_direction(c) != NONE);

    if (is_player)
        return PLAYER;

    if (is_monster)
        return MONSTER;

    return NO_ENT;
}

t_entity create_entity(t_entity_type type, int x, int y, t_direction dir) {
    t_entity entity;

    t_position pos;
    pos.x = x;
    pos.y = y;

    entity.type = type;
    entity.pos = pos;
    entity.facing_dir = dir;

    return entity;
}

int check_wall(t_board *board, t_position *new_pos) {
    if (board->cells[new_pos->y][new_pos->x] == "https://codereview.stackexchange.com/questions/267757/#")
        return 1;
        
    return 0;
}

int check_valid_move(t_board *board, t_position *new_pos) {
    if (new_pos->x >= board->col_size || new_pos->x < 0)
        return 0;
    
    if (new_pos->y >= board->num_rows || new_pos->y < 0)
        return 0;

    return 1;
}

void handle_collision(t_board *board, t_entity *entity, t_position *new_pos) {
    t_position old_pos = entity->pos;

    if (!check_valid_move(board, new_pos)) {
        *new_pos = old_pos;
        return;
    }

    int collided_with_wall = check_wall(board, new_pos);

    if (entity->type == MONSTER) {
        if (collided_with_wall || get_cell_at(board, new_pos->x, new_pos->y) == 'A') {
            entity->facing_dir = get_opposite_direction(entity->facing_dir);
            *new_pos = old_pos;
            char c = map_direction_to_char(entity->facing_dir);
            set_cell_at(board, new_pos->x, new_pos->y, c);
            return;
        }
    }

    if (entity->type == PLAYER && collided_with_wall) {
        *new_pos = old_pos;
    }
}

int compare_positions(t_position *pos1, t_position* pos2) {
    if (pos1->y < pos2->y)
        return -1;

    if (pos1->y > pos2->y)
        return 2;

    if (pos1->x < pos2->x)
        return -1;

    if (pos1->x > pos2->x)
        return 1;

    return 0;
}

void move_entity(t_board *board, t_entity *entity, t_direction dir) {
    t_position old_pos;
    old_pos.x = entity->pos.x;
    old_pos.y = entity->pos.y;

    t_position new_pos = old_pos;
    t_direction new_dir = dir;

    if (entity->type == MONSTER) {
        new_dir = entity->facing_dir;
    }

    switch (new_dir) {
        case UPWARDS:
            new_pos.y--;
            break;
        case LEFT:
            new_pos.x--;
            break;
        case DOWNWARDS:
            new_pos.y++;
            break;
        case RIGHT:
            new_pos.x++;
            break;
        case NONE:
            break;
    }

    handle_collision(board, entity, &new_pos);
    
    entity->pos = new_pos;
}

board.h

#ifndef BOARD_H
#define BOARD_H

#include "game_params.h"
#include "direction.h"
#include <stdio.h>

typedef struct position t_position;
typedef struct entity t_entity;

typedef enum cell_type {
    ENTITY,
    WALL,
    EMPTY
} t_cell_type;

typedef struct board {
    int num_rows;
    int col_size;
    char **cells;
    
    int num_entities;
    int player_index;
    t_entity *entities;
    t_position *goal_positions;
} t_board;

void cleanup_board(t_board *board);

char get_cell_at(t_board *board, int x, int y);
void set_cell_at(t_board *board, int x, int y, char c);

t_cell_type get_cell_type(char c);

void clear_entities_from_board(t_board *board);
void place_entities_on_board(t_board *board);
void print_board(t_board *b, FILE *output);

void get_board_dims(char *buf, int *num_rows, int *col_size);
t_error_object fill_board(t_board *board, char *board_data, int len);
t_error_object handle_entity_alloc(t_board *board, const t_entity *entity,
                                int *actual_entity_count, int *expected_entity_count);
t_error_object set_initial_positions(t_board *board);
t_error_object initialize_board(t_board* board, const t_game_params *params);

#endif

board.c

#include "common.h"
#include "entity.h"
#include "board.h"

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

void cleanup_board(t_board *board) {
    for (int i = 0; i < board->num_rows; i++) {
        if (board->cells[i] != NULL)
            free(board->cells[i]);
    }

    if (board->cells != NULL)
        free(board->cells);

    if (board->entities)
        free(board->entities);
}

char get_cell_at(t_board* board, int x, int y) {
    return board->cells[y][x];
}

void set_cell_at(t_board *board, int x, int y, char c) {
    board->cells[y][x] = c;
}

void clear_entities_from_board(t_board *board) {
    for (int i = 0; i < board->num_entities; i++) {
        t_entity ent = board->entities[i];
        set_cell_at(board, ent.pos.x, ent.pos.y, ' ');
    }
}

void place_entities_on_board(t_board *board) {
    // First draw Player (S)
    t_entity *player = &board->entities[board->player_index];

    // 'A' always stays on top of 'S' when they overlap
    if (get_cell_at(board, player->pos.x, player->pos.y) != 'A')
        set_cell_at(board, player->pos.x, player->pos.y, 'S');

    // Then draw Monsters (M) in reverse (right-to-left)
    // to satisfy the condition that monsters seen earlier
    // should appear before monsters seen at a later point
    // in case some monsters overlap at a single position

    for (int i = board->num_entities - 1; i >= 0; i--) {
        t_entity ent = board->entities[i];

        char symbol=" ";

        if (ent.type != MONSTER)
            continue;
        
        symbol = map_direction_to_char(ent.facing_dir);

        set_cell_at(board, ent.pos.x, ent.pos.y, symbol);
    }
}

void print_board(t_board *board, FILE *output) {
    place_entities_on_board(board);

    for (int row = 0; row < board->num_rows; row++) {
        for (int col = 0; col < board->col_size; col++) {
            char c = board->cells[row][col];

            if (c != 0)
                fputc(c, output);
        }

        fputc('n', output);
    }

    clear_entities_from_board(board);
}

void get_board_dims(char *buf, int *num_rows, int *col_size) {
    int num_lines = 0;
    int longest_line_len = 0;

    char* buf_copy = strdup_(buf);
    char* pch = strtok(buf_copy, "n");

    while (pch != NULL) {
        num_lines++;

        if (strlen(pch) > longest_line_len)
            longest_line_len = strlen(pch);

        pch = strtok(NULL, "n");
    }

    free(buf_copy);
    buf_copy = NULL;

    *num_rows = num_lines;
    *col_size = longest_line_len;
}

t_error_object fill_board(t_board *board, char *board_data, int len) {
    int cur_row = 0;
    int cur_col = 0;

    char **b = calloc(board->num_rows, sizeof(char*));

    if (b == NULL) {
        return make_error("Konnte keinen Speicherplatz für das Gameboard allozieren", ALLOC_FAILED);
    }

    for (int i = 0; i < board->num_rows; i++) {
        b[i] = calloc(board->col_size, sizeof(char));

        if (b[i] == NULL) {
            return make_error("Konnte keinen Speicherplatz für das Gameboard allozieren", ALLOC_FAILED);
        }
    }

    for (int i = 0; i < len; i++) {
        if (board_data[i] == 'n') {
            cur_row++;
            cur_col = 0;
            continue;
        }

        b[cur_row][cur_col] = board_data[i];
        cur_col++;
    }

    free(board_data);

    board->cells = b;

    return make_error("", OK);
}

t_error_object handle_entity_alloc(t_board *board, const t_entity *entity,
                        int *actual_entity_count, int *expected_entity_count) {
    *actual_entity_count += 1;

    if (*actual_entity_count > *expected_entity_count) {
        *expected_entity_count = *expected_entity_count * 2 + 1;
        board->entities = realloc(board->entities, *expected_entity_count * sizeof(t_entity));
    }

    if (board->entities == NULL) {
        return make_error("Konnte keinen Speicherplatz für die Entitäten allozieren", ALLOC_FAILED);
    }

    board->entities[*actual_entity_count - 1] = *entity;

    return make_error("", OK);
}

t_cell_type get_cell_type(char c) {
    t_entity_type ent_type = get_entity_type(c);

    int is_wall = (c == "https://codereview.stackexchange.com/questions/267757/#");
    int is_empty = (c == ' ');

    if (ent_type != NO_ENT)
        return ENTITY;

    if (is_wall)
        return WALL;

    if (is_empty)
        return EMPTY;

    return EMPTY;
}

t_error_object set_initial_positions(t_board *board) {
    int expected_entity_count = 1;
    int actual_entity_count = 0;

    board->entities = calloc(expected_entity_count, sizeof(t_entity));
    
    if(board->entities == NULL) {
        return make_error("Konnte keinen Speicherplatz für die Entitäten allozieren", ALLOC_FAILED);
    }

    for (int y = 0; y < board->num_rows; y++) {
        for (int x = 0; x < board->col_size; x++) {
            int c = board->cells[y][x];
            t_cell_type type = get_cell_type(c);

            if (type != ENTITY)
                continue;

            t_entity_type ent_type = get_entity_type(c);
            t_direction ent_dir = map_char_to_direction(c);
            t_entity ent = create_entity(ent_type, x, y, ent_dir);

            t_error_object ret = handle_entity_alloc(board, &ent, &actual_entity_count, &expected_entity_count);

            if (ret.error_code != OK)
                return ret;

            if (ent_type == PLAYER)
                board->player_index = actual_entity_count - 1;
        }
    }

    board->num_entities = actual_entity_count;

    return make_error("", OK);
}

t_error_object initialize_board(t_board *board, const t_game_params *params) {
    int num_rows;
    int col_size;

    int file_size = get_file_size(params->level_file);
    char *level_data = calloc(file_size + 1, sizeof(char));

    if (level_data == NULL) {
        return make_error("Konnte keinen Speicherplatz für das Gameboard allozieren", ALLOC_FAILED);
    }

    fread(level_data, file_size, 1, params->level_file);

    if (ferror(params->level_file) != 0) {
        return make_error("Konnte Level-Datei nicht lesen", COULD_NOT_READ_FILE);
    }

    get_board_dims(level_data, &num_rows, &col_size);

    board->num_rows = num_rows;
    board->col_size = col_size;

    fill_board(board, level_data, file_size);
    set_initial_positions(board);

    return make_error("", OK);
}

direction.h

#ifndef DIRECTION_H
#define DIRECTION_H

typedef enum direction {
    UPWARDS,
    LEFT,
    DOWNWARDS,
    RIGHT,
    NONE
} t_direction;

char map_direction_to_char(t_direction dir);
t_direction map_char_to_direction(char dir);
t_direction get_opposite_direction(t_direction dir);

#endif

direction.c

#include "direction.h"

char map_direction_to_char(t_direction dir) {
    switch (dir) {
        case UPWARDS:
            return '^';
        case LEFT:
            return '<';
        case DOWNWARDS:
            return 'v';
        case RIGHT:
            return '>';
        case NONE:
            return 0;
    }

    return 0;
}

t_direction map_char_to_direction(char dir) {
    switch (dir) {
        case '^':
        case 'w':
            return UPWARDS;
        case '<':
        case 'a':
            return LEFT;
        case 'v':
        case 's':
            return DOWNWARDS;
        case '>':
        case 'd':
            return RIGHT;
    }

    return NONE;
}

t_direction get_opposite_direction(t_direction dir) {
    switch (dir) {
        case UPWARDS:
            return DOWNWARDS;
        case LEFT:
            return RIGHT;
        case DOWNWARDS:
            return UPWARDS;
        case RIGHT:
            return LEFT;
        case NONE:
            return NONE;
    }

    return NONE;
}

dungeon.h

#define DUNGEON_H

#include "game_params.h"
#include "board.h"

typedef enum game_status {
    RUNNING,
    WON,
    LOST
} t_game_status;

void cleanup(t_game_params *params, t_board *board);
t_game_status check_win_or_death(t_board *board);
void game_loop(t_board *board, t_game_params *params);
int main(int argc, char **argv);

#endif

dungeon.c

#include "common.h"
#include "direction.h"
#include "entity.h"
#include "board.h"
#include "dungeon.h"

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

void cleanup(t_game_params *params, t_board *board) {
    cleanup_game_files(params);
    cleanup_board(board);
}

t_game_status check_win_or_death(t_board *board) {
    t_entity *player = &board->entities[board->player_index];

    if (get_cell_at(board, player->pos.x, player->pos.y) == 'A')
        return WON;

    for (int i = 0; i < board->num_entities; i++) {
        t_entity *ent = &board->entities[i];

        if (ent->type == PLAYER)
            continue;

        int positions_match = compare_positions(&player->pos, &ent->pos) == 0;

        if (positions_match && ent->type == MONSTER)
            return LOST;
    }

    return RUNNING; 
}

void game_loop(t_board *board, t_game_params *params) {
    FILE *input_stream = params->input_file;
    FILE *output_stream = params->output_file;

    int step = 1;
    char command = 0;

    t_game_status game_status = RUNNING;

    while (1) {

        fprintf(output_stream, "%d ", step);
        fscanf(input_stream, " %c", &command);

        if (input_stream != stdin) {
            fprintf(output_stream, "%c", command);
            fprintf(output_stream, "n");
        }

        t_direction dir = map_char_to_direction(command);

        for (int i = 0; i < board->num_entities; i++) {
            t_entity *ent = &board->entities[i];
            move_entity(board, ent, dir);
        }

        game_status = check_win_or_death(board);

        print_board(board, params->output_file);

        if (game_status != RUNNING)
            break;
            
        step++;
    }

    if (game_status == LOST)
        fprintf(output_stream, "Du wurdest von einem Monster gefressen.n");
    else if (game_status == WON)
        fprintf(output_stream, "Gewonnen!n");
}

int main(int argc, char **argv) {
    t_game_params params =  {NULL, NULL, NULL};
    t_board board =  {0, 0, NULL, 0, 0, NULL, NULL};

    t_error_object err;

    err = parse_game_parameters(&params, argc, argv);
    
    if (err.error_code != OK) {
        cleanup(&params, &board);
        fprintf(stderr, "%s, error_code: %dn", err.msg, err.error_code);
        return err.error_code;
    }

    err = initialize_board(&board, &params);

    if (err.error_code != OK) {
        cleanup(&params, &board);
        fprintf(stderr, "%s, error_code: %dn", err.msg, err.error_code);
        return err.error_code;
    }

    print_board(&board, params.output_file);
    game_loop(&board, &params);

    cleanup(&params, &board);

    return 0;
}

Вопросов

  • Как я могу более кратко справиться с очисткой ресурсов? На данный момент я пытаюсь имитировать обработку исключений, позволяя ошибкам пузыриться до main и делаю там генеральную уборку. Я подумал о передаче структуры (шаблона распределителя) функциям, генерирующим ошибки.
  • Лучшая обработка ошибок
  • Должен ли я вместо того, чтобы «злоупотреблять» игровой доской как для рисования, так и для проверки коллизий, заключать ячейки в пользовательскую структуру данных?
  • Я все еще работаю над исправлением const правильность здесь и там.
  • Это direction абстракция — хороший паттерн или бесполезное раздувание моей кодовой базы?
  • Есть ли лучшая структура данных для представления моей игровой доски и динамических объектов?
  • Объединение коллизионных и выигрышных чеков. Я использую состояние холста для проверки наличия столкновений и выигрыша, но сравниваю позиции «виртуального» игрока и монстра, чтобы проверить, нет ли проигрыша.

2 ответа
2

Ответы на ваши вопросы

Как я могу более кратко справиться с очисткой ресурсов? На данный момент я пытаюсь имитировать обработку исключений, позволяя ошибкам пузыриться до main и делаю там генеральную уборку. Я подумал о передаче структуры (шаблона распределителя) функциям, вызывающим ошибки.

C ++ делает это много проще, с RAII и языковая поддержка для исключения. В C, позволяя ошибкам «всплывать» и позволяя main() делать очистку работает только если main() выполнил все распределения или может каким-то образом увидеть распределения, выполненные другими функциями. В более крупных программах это обычно не лучшая стратегия.

Разделите ошибки на две категории:

  1. Неустранимые ошибки, такие как невыполнение выделения памяти или невозможность чтения необходимого файла. В этом случае просто распечатайте сообщение об ошибке на stderr и позвони exit(EXIT_FAILURE).

  2. Исправимые ошибки. Используйте тип возврата, который может указывать на состояние ошибки, например bool представляющий успех или неудачу, целое число или перечисление с кодом ошибки, или если вы возвращаете указатель на объект, NULL может означать неудачу. Затем вызывающий абонент может решить, как исправить эту ошибку.

Должен ли я вместо того, чтобы «злоупотреблять» игровой доской как для рисования, так и для проверки столкновений, заключать ячейки в пользовательскую структуру данных?

Наличие специального типа для ячеек — действительно хорошая идея.

Я все еще работаю над исправлением правильности const здесь и там.

Да, можно привести множество аргументов функции const.

Абстракция направления — хороший образец или бесполезное раздувание моей кодовой базы?

Я не думаю, что это добавляет столько раздувания, но есть другие способы справиться с этим. Рассмотрите возможность создания struct который хранит направление как координаты x и y, например:

typedef struct direction {
    int8_t dx;
    int8_t dy;
} t_direction;

Тогда например в move_entity(), вам больше не нужен switch-запись, но можно просто написать:

new_pos.x = old_pos.x + new_dir.dx;
new_pos.y = old_pos.y + new_dir.dy;

Есть ли лучшая структура данных для представления моей игровой доски и динамических объектов?

Есть много способов сохранить доску и объекты со своими плюсами и минусами. У вас есть то преимущество, что и распечатать доску, и найти то, что находится в данной позиции, очень легко. Если рассматривать цель как динамическую сущность, то единственными статичными объектами остаются стены. Итак, вы можете использовать битовый массив чтобы сохранить его только в одной восьмой объема памяти, который вы используете в настоящее время, и при этом иметь возможность быстрого обнаружения столкновений со стенами. Однако печать платы была бы немного сложнее.

Объединение коллизионных и выигрышных чеков. Я использую состояние холста для проверки наличия столкновений и выигрыша, но сравниваю позиции «виртуального» игрока и монстра, чтобы проверить, нет ли проигрыша.

Да, в идеале при обновлении врагов и игрока ставить game_status если они сталкиваются друг с другом, или если игрок сталкивается с воротами.

Небезопасное использование strncpy()

При звонке make_error(), вы копируете строку message в массив msg[100] с использованием strncpy(). Однако если длина message было 100 или более символов, тогда strncpy() не будет записывать NUL-байт в конце msg[]. Либо напишите NUL-байт в msg[sizeof(msg) - 1] безоговорочно или используйте более безопасную функцию для записи в msg[], нравиться snprintf().

Или вообще не делайте копию. Вы только когда-либо звоните make_error() со строковым литералом, поэтому вы можете просто сохранить указатель на строку в t_error_object. Это также сделало бы этот объект намного более легким.

Вводящее в заблуждение сообщение об ошибке

Если имя входного файла не содержит «.txt» где-либо в имени файла, вы возвращаете ошибку, которая переводится как «входной файл не может быть прочитан». Однако с файлом все в порядке. Либо не ограничивайте имя файла, либо верните сообщение об ошибке, в котором говорится, что имя файла должно заканчиваться на «.txt».

Предпочитать bool для истинных / ложных результатов

Функция вроде check_valid_move() должен вернуть bool для указания истинных или ложных значений.

  • Большое спасибо за обстоятельные ответы. У меня вопрос по поводу этого утверждения: «Неустранимые ошибки, […] напечатайте сообщение об ошибке в stderr и вызовите exit (EXIT_FAILURE). «Означает ли это, что мне не нужно выполнять какую-либо очистку, если я просто вызываю exit? Весь смысл введения структур ошибок состоял в том, чтобы иметь возможность вызывать функцию очистки, не передавая указатель на все ресурсы каждой функции, которая может вызвать ошибку.

    — never_feel_mellow



  • 1

    Открытые файлы и выделенная память автоматически очищаются операционной системой (я предполагаю, что даже на консоли, для которой вы пишете свою игру), когда программа завершается.

    — Г. Сон

  • Хорошо, это упростит жизнь!

    — never_feel_mellow

char *strdup_(const char *src) {
    char *dst = malloc(strlen (src) + 1);  
    if (dst == NULL) return NULL;          
    strcpy(dst, src);                      
    return dst;                            
}

Вы звоните strlen чтобы выделить память, а затем вызов strcpy который работает по вызову strlen первый! Запомните длину и используйте memcpy вместо.

void cleanup_board(t_board *board) {
    for (int i = 0; i < board->num_rows; i++) {
        if (board->cells[i] != NULL)
            free(board->cells[i]);
    }

    if (board->cells != NULL)
        free(board->cells);

    if (board->entities)
        free(board->entities);
}

Ваш NULL везде проверка не нужна, так как free уже есть такая проверка!

t_error_object fill_board(t_board *board, char *board_data, int len) {

Эта функция freeс board_data, который является переданным параметром, а не чем-то выделенным. В C нужно быть осторожным с обязанностями владения! Это включает в себя аккуратное распределение и освобождение в одном и том же месте, а также использование соглашений об именах относительно того, когда возвращается копия, когда сохраняется право собственности на параметр и т. Д. Это действительно проклятие существования в мире C.

Не кажется логичным, что это действительно «набить плату, а параметр тем временем уничтожить». И что за len для?

В этой функции board->cells перезаписывается, но старое значение не освобождается. Если это называть что-то вроде initialize_board вместо этого, поскольку он должен вызываться только тогда, когда доска либо недавно объявлена, либо после уничтожения?

return make_error("", OK);

Просто укажите, что это скопирует 100 '' персонажей в структуру.


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


Вы перепечатываете доску на каждом шагу. Как насчет использования управляющих кодов для изменения положения курсора, чтобы вы могли каждый раз рисовать на месте?


Вместо того, чтобы ваш двумерный массив был массивом указателей на индивидуально выделенные строки, вы могли бы использовать один указатель и выделение для всего блока (строки × столбцы). Вы уже абстрагировались от функций get и set, поэтому просто измените их, чтобы вычислить row * col_count + column явно.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *