Вступление
Я начал немного изучать программирование на 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(¶ms, argc, argv);
if (err.error_code != OK) {
cleanup(¶ms, &board);
fprintf(stderr, "%s, error_code: %dn", err.msg, err.error_code);
return err.error_code;
}
err = initialize_board(&board, ¶ms);
if (err.error_code != OK) {
cleanup(¶ms, &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, ¶ms);
cleanup(¶ms, &board);
return 0;
}
Вопросов
- Как я могу более кратко справиться с очисткой ресурсов? На данный момент я пытаюсь имитировать обработку исключений, позволяя ошибкам пузыриться до
main
и делаю там генеральную уборку. Я подумал о передаче структуры (шаблона распределителя) функциям, генерирующим ошибки. - Лучшая обработка ошибок
- Должен ли я вместо того, чтобы «злоупотреблять» игровой доской как для рисования, так и для проверки коллизий, заключать ячейки в пользовательскую структуру данных?
- Я все еще работаю над исправлением
const
правильность здесь и там. - Это
direction
абстракция — хороший паттерн или бесполезное раздувание моей кодовой базы? - Есть ли лучшая структура данных для представления моей игровой доски и динамических объектов?
- Объединение коллизионных и выигрышных чеков. Я использую состояние холста для проверки наличия столкновений и выигрыша, но сравниваю позиции «виртуального» игрока и монстра, чтобы проверить, нет ли проигрыша.
2 ответа
Ответы на ваши вопросы
Как я могу более кратко справиться с очисткой ресурсов? На данный момент я пытаюсь имитировать обработку исключений, позволяя ошибкам пузыриться до
main
и делаю там генеральную уборку. Я подумал о передаче структуры (шаблона распределителя) функциям, вызывающим ошибки.
C ++ делает это много проще, с RAII и языковая поддержка для исключения. В C, позволяя ошибкам «всплывать» и позволяя main()
делать очистку работает только если main()
выполнил все распределения или может каким-то образом увидеть распределения, выполненные другими функциями. В более крупных программах это обычно не лучшая стратегия.
Разделите ошибки на две категории:
Неустранимые ошибки, такие как невыполнение выделения памяти или невозможность чтения необходимого файла. В этом случае просто распечатайте сообщение об ошибке на
stderr
и позвониexit(EXIT_FAILURE)
.Исправимые ошибки. Используйте тип возврата, который может указывать на состояние ошибки, например
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
для указания истинных или ложных значений.
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
явно.
Большое спасибо за обстоятельные ответы. У меня вопрос по поводу этого утверждения: «Неустранимые ошибки, […] напечатайте сообщение об ошибке в stderr и вызовите exit (EXIT_FAILURE). «Означает ли это, что мне не нужно выполнять какую-либо очистку, если я просто вызываю
exit
? Весь смысл введения структур ошибок состоял в том, чтобы иметь возможность вызывать функцию очистки, не передавая указатель на все ресурсы каждой функции, которая может вызвать ошибку.— never_feel_mellow
Открытые файлы и выделенная память автоматически очищаются операционной системой (я предполагаю, что даже на консоли, для которой вы пишете свою игру), когда программа завершается.
— Г. Сон
Хорошо, это упростит жизнь!
— never_feel_mellow