Использование многомерных массивов в C на самом деле больше боли, чем они того стоят. Гораздо лучшим вариантом является использование структуры с динамически распределенным массивом, описывающим лабиринт, и функций доступа для изучения и изменения ячеек лабиринта. Вместо того, чтобы помещать маркеры в данные лабиринта, вы можете поместить координаты начала / конца / текущего местоположения в структуру.
(я понимаю, что это не отвечает на поставленный ОП вопрос, но является ответом на основную проблему, которую ОП пытается решить.)
Рассмотрим следующий пример. Он ограничивает размер лабиринта 255 × 255, но поскольку каждая координата и ячейка лабиринта всегда составляют всего один байт, файлы сохранения переносимы между архитектурами, так как нет порядка байтов (порядка байтов), о котором следует беспокоиться. (Вам, как программисту, все же нужно выбрать использовать только коды 0..255 в лабиринте, чтобы сохранить переносимость данных; приведенные ниже функции не будут обеспечивать это.)
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
typedef struct {
unsigned char row;
unsigned char col;
} mazepoint;
typedef struct {
unsigned char rows;
unsigned char cols;
unsigned char *cell;
mazepoint player;
} maze;
#define OUTSIDE 0
static inline unsigned char maze_get(maze *const m,
const int row,
const int col)
{
if (m &&
row >= 0 && row < (int)(m->rows) &&
col >= 0 && col < (int)(m->cols))
return m->cell[ (size_t)col + (size_t)(m->cols) * (size_t)row ];
else
return OUTSIDE;
}
static inline unsigned char maze_set(maze *const m,
const int row,
const int col,
const unsigned char val)
{
if (m &&
row >= 0 && row < (int)(m->rows) &&
col >= 0 && col < (int)(m->cols))
return m->cell[ (size_t)col + (size_t)(m->cols) * (size_t)row ] = val;
else
return OUTSIDE;
}
static inline void maze_free(maze *const m)
{
if (m) {
free(m->cell);
m->rows = 0;
m->cols = 0;
m->cell = NULL;
}
}
int maze_create(maze *const m,
const int rows,
const int cols)
{
size_t cells = (size_t)rows * (size_t)cols;
unsigned char *cell;
if (!m)
return -1; /* NULL reference to a maze variable! */
if (rows < 1 || rows > 255 ||
cols < 1 || cols > 255)
return -1; /* Invalid number of rows or columns! */
cell = malloc(cells); /* sizeof (unsigned char) == 1. */
if (!cell)
return -1;
/* Initialize all maze cells to OUTSIDE. */
memset(cell, OUTSIDE, cells);
m->rows = rows;
m->cols = cols;
m->cell = cell;
/* Let's initialize player location to upper left corner. */
m->player.row = 0;
m->player.col = 0;
return 0; /* Success. */
}
int maze_save(maze *const m, const char *filename)
{
size_t cells;
FILE *out;
if (!m || m->rows < 1 || m->cols < 1)
return -1; /* No maze to save! */
if (!filename || !filename[0])
return -1; /* NULL or empty filename! */
cells = (size_t)(m->rows) * (size_t)(m->cols);
out = fopen(filename, "wb");
if (!out)
return -1; /* Cannot open file for writing! */
do {
/* First byte is the number of rows. */
if (fputc(m->rows, out) == EOF)
break;
/* Second byte is the number of columns. */
if (fputc(m->cols, out) == EOF)
break;
/* rows*cols bytes of maze data follows. */
if (fwrite(m->cell, 1, cells, out) != cells)
break;
/* Player location follows. */
if (fputc(m->player.row, out) == EOF)
break;
if (fputc(m->player.col, out) == EOF)
break;
/* You can save additional data at this point. */
/* That completes the save file. Ensure it is correctly saved. */
if (fflush(out))
break;
if (fclose(out))
break;
/* Maze successfully saved. */
return 0;
} while (0);
/* Save failed. */
fclose(out);
remove(filename);
return -1;
}
int maze_load(maze *const m, const char *filename)
{
size_t cells;
unsigned char *cell;
int rows, cols, r, c;
FILE *in;
if (!m)
return -1; /* No reference to a maze variable to load into! */
/* Just in case, we clear the maze first. Might help finding bugs! */
m->rows = 0;
m->cols = 0;
m->cell = NULL;
if (!filename || !filename[0])
return -1; /* NULL or empty filename! */
in = fopen(filename, "rb");
if (!in)
return -1; /* Cannot open file for reading. */
rows = fgetc(in);
cols = fgetc(in);
if (rows == EOF || rows < 1 || rows > 255 ||
cols == EOF || cols < 1 || cols > 255) {
fclose(in);
return -1; /* Not a saved maze! */
}
cells = (size_t)(rows) * (size_t)(cols);
cell = malloc(cells);
if (!cell) {
fclose(in);
return -1; /* Not enough memory available! */
}
do {
/* Read maze cell data. */
if (fread(cell, 1, cells, in) != cells)
break;
/* Player location. */
r = fgetc(in);
c = fgetc(in);
if (r == EOF || r < 0 || r > 255 ||
c == EOF || c < 0 || c > 255)
break;
m->player.row = r;
m->player.col = c;
/* Load other saved data here. */
/* All data read successfully. */
fclose(in);
m->rows = rows;
m->cols = cols;
m->cell = cell;
return 0;
} while (0);
/* Read error. */
fclose(in);
free(cell);
return -1;
}
В вашей собственной программе вы бы создали лабиринт следующим образом:
maze m;
/* Create a 20-row, 30-column maze. */
if (maze_create(&m, 20, 30)) {
/* Failed to create maze! Show an error message. */
exit(EXIT_FAILURE);
}
Чтобы сохранить лабиринт, чтобы сказать maze.dat
, вы используете
m.player.row = /* row where the player is */
m.player.col = /* column where the player is */
if (maze_save(&m, "maze.dat")) {
/* Failed! Show an error message. */
exit(EXIT_FAILURE);
}
Если вы посмотрите на пример кода, вы можете добавить дополнительные данные, особенно точки, такие как player
, для сохранения и загрузки вместе с самими ячейками лабиринта.
Чтобы уничтожить лабиринт, когда он больше не нужен, используйте
maze_free(&m);
Чтобы загрузить сохраненный лабиринт, скажем, из maze.dat
, используйте
if (maze_load(&m, "maze.dat")) {
/* Failed! Show an error message. */
exit(EXIT_FAILURE);
}
/* Restore player place from m.player.row and m.player.col */
Функция доступа maze_get()
не ограничена действительными координатами (от 0 до rows-1
или cols-1
включительно). Если вы изучите за пределами самого лабиринта, он просто вернет значение макроса OUTSIDE
. Например,
if (maze_get(&m, row, col) == 5) {
/* That cell has value 5 */
} else {
/* Either the cell has a different value,
or row,col is outside the maze. */
}
Точно так же вы можете попытаться установить любое значение ячейки безопасно. Однако он будет «прилипать» только в том случае, если он находится в допустимом диапазоне координат лабиринта; в другом месте он вернет OUTSIDE
:
if (maze_set(&m, row, col, 5) == 5) {
/* Changed cell value to 5 */
} else {
/* row,col is outside the maze. */
}
Причина, по которой я так написал макросы доступа, заключается в том, что он делает рендеринг только части лабиринта очень простым. Если размер представления viewrows
на viewcols
с центром в row
и col
, вы можете визуализировать представление с помощью простого цикла:
const int top = row - viewrows / 2;
const int left = col - viewcols / 2;
int vr, vc;
for (vr = 0; vr < viewrows; vr++) {
for (vc = 0; vc < viewcols; vc++) {
const unsigned char v = maze_get(&m, top+vr, left+vc);
/* Draw v at row vr, col vc */
}
}
и ячейки отображаются даже в том же порядке, в каком вы читали этот текст; сверху вниз, слева направо.
Обратите внимание, что вместо использования значений ячеек лабиринта для кодов символов, вы должны использовать таблицу поиска. Например,
int cell_char[256];
Вместо прямой печати значений ячеек вы напечатаете соответствующий cell_char
, например
fputc(cell_char[maze_get(&m, row, col)], stdout);
Таким образом, вы можете группировать, например, различные символы стены в последовательном диапазоне, или даже использовать отдельные биты в 8-битном значении ячейки в качестве идентификаторов. Затем ячейки лабиринта описывают логическое содержимое в этой ячейке лабиринта, а не ее визуальное представление, с логически-визуальным отображением в отдельном массиве.
Если бы вы использовали Gtk +, вы могли бы иметь массив указателей GtkImage,
GtkImage *cell_image[256] = {0}; /* All NULL by default */
или используя SDL, вы можете использовать лабиринт в качестве текстур, которые вы можете рендерить,
SDL_Texture *cell_texture[256] = {0}; /* All NULL by default */
и в обоих случаях считывайте их либо с одного большого изображения (скажем, разделенного на прямоугольники с одинаковым размером 16 × 16), либо из отдельных файлов изображений.
Например, вы можете решить, что четыре младших значащих бита в значении ячейки указывают, является ли перемещение из этой ячейки вверх (предыдущая строка), вниз (следующая строка), влево (предыдущий столбец) или вправо (следующий столбец) возможно:
#define CAN_GO_UP(value) ((value) & (1 << 0)) /* == 1 */
#define CAN_GO_DOWN(value) ((value) & (1 << 1)) /* == 2 */
#define CAN_GO_LEFT(value) ((value) & (1 << 2)) /* == 4 */
#define CAN_GO_RIGHT(value) ((value) & (1 << 3)) /* == 8 */
Обратите внимание, что это позволяет вам делать «ловушки»: проходы, которые работают только в одном направлении. Значения ячеек лабиринта, кратные 16 (0, 16, 32, 48, 64, 80, 96, ..., 208, 224 и 240), представляют собой полностью заблокированные ячейки: выхода нет. +1 позволяет пройти вверх; +2 позволяет проход вниз; +3 позволяет проход вверх и вниз; +4 разрешает проход слева; +5 позволяет проход влево и вверх; +6 позволяет проход влево и вниз; +7 позволяет проход вверх, влево и вниз; +8 позволяет проход вправо; +9 позволяет проход вверх и вправо; +10 позволяет проход вниз и вправо; +11 позволяет проход вверх, вниз и вправо; +12 позволяет проход влево и вправо; +13 позволяет проход вверх, влево и вправо; +14 позволяет проход вниз, влево и вправо; +15 позволяет проход вверх, вниз, влево и вправо.
Я бы лично порекомендовал использовать широкую версию библиотеки ncurses (ncursesw). (Я не использую Windows, поэтому я не совсем уверен, как вы устанавливаете и используете его в Windows, но домашняя страница ncurses имеет загрузки при использовании mingw .)
Тогда у вас будет гораздо более широкий выбор глифов, которые вы можете использовать. (При использовании локалей UTF-8 потенциально весь набор символов Unicode - блок Box Drawing особенно полезен для рисования лабиринта, и большинство из этих символов также доступны в старом CP437 * Кодовая страница 1080 *, что означает, что они должны хорошо работать как в Windows, так и в Windows-терминалах.)
В этом случае вы, вероятно, будете использовать
cchar_t cell_char[256];
Как я упоминал выше, вы даже можете создать графическую версию (возможно, позже, расширив версию терминала) на C, используя SDL или GTK + . (Обратите внимание, что приведенное выше разделение между значением содержимого логической ячейки лабиринта и визуальным описанием ячейки также означает, что вы можете во время выполнения выбирать между «темами», имея более одного набора изображений ячеек. Это позволяет вам начать с грубой информационной версии, для отладки, а затем добавить визуальное совершенство.)
Подход, показанный в этих ответах, позволяет начать с простой игры на основе терминала, и, если вы решите, что хотите, добавьте поддержку графического интерфейса пользователя с лабиринтными ячейками на основе изображений без необходимости переписывать ядро. код лабиринта.