feat(maze): add deterministic symmetric maze generation
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
idf_component_register(
|
||||
SRCS "maze32_maze.c"
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
#ifndef MAZE32_MAZE_H
|
||||
#define MAZE32_MAZE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAZE32_MAZE_MAX_WIDTH 128U
|
||||
#define MAZE32_MAZE_MAX_HEIGHT 64U
|
||||
#define MAZE32_MAZE_MAX_CELLS (MAZE32_MAZE_MAX_WIDTH * MAZE32_MAZE_MAX_HEIGHT)
|
||||
#define MAZE32_MAZE_DEFAULT_WIDTH 128U
|
||||
#define MAZE32_MAZE_DEFAULT_HEIGHT 64U
|
||||
#define MAZE32_MAZE_MAX_LANDMARK_PAIRS 16U
|
||||
#define MAZE32_MAZE_MAX_ARTIFACT_PAIRS 16U
|
||||
#define MAZE32_MAZE_RAM_BUDGET_BYTES (64U * 1024U)
|
||||
#define MAZE32_MAZE_RUNTIME_BUDGET_MS 250U
|
||||
|
||||
typedef enum {
|
||||
MAZE32_PASSAGE_NORTH = 1U << 0U,
|
||||
MAZE32_PASSAGE_EAST = 1U << 1U,
|
||||
MAZE32_PASSAGE_SOUTH = 1U << 2U,
|
||||
MAZE32_PASSAGE_WEST = 1U << 3U,
|
||||
} maze32_passage_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
} maze32_cell_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t initial_view_radius;
|
||||
uint8_t cross_half_pairs;
|
||||
uint16_t extra_cycle_pairs;
|
||||
uint16_t minimum_exit_separation;
|
||||
uint8_t landmark_pairs;
|
||||
uint8_t artifact_pairs;
|
||||
uint8_t maximum_attempts;
|
||||
} maze32_maze_config_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_cell_t cells[2];
|
||||
uint8_t kind;
|
||||
} maze32_landmark_pair_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_maze_config_t config;
|
||||
uint32_t requested_seed;
|
||||
uint32_t generation_seed;
|
||||
uint8_t generation_attempts;
|
||||
uint8_t passages[MAZE32_MAZE_MAX_CELLS];
|
||||
maze32_cell_t starts[2];
|
||||
maze32_cell_t keys[2];
|
||||
maze32_cell_t exits[2];
|
||||
maze32_cell_t compasses[2];
|
||||
maze32_landmark_pair_t landmarks[MAZE32_MAZE_MAX_LANDMARK_PAIRS];
|
||||
maze32_cell_t artifacts[MAZE32_MAZE_MAX_ARTIFACT_PAIRS][2];
|
||||
} maze32_maze_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t visited[(MAZE32_MAZE_MAX_CELLS + 7U) / 8U];
|
||||
uint16_t traversal[MAZE32_MAZE_MAX_CELLS];
|
||||
uint16_t distances[MAZE32_MAZE_MAX_CELLS];
|
||||
} maze32_maze_workspace_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t reachable_cells;
|
||||
uint16_t passage_edges;
|
||||
uint16_t cycle_count;
|
||||
uint16_t dead_end_count;
|
||||
uint16_t branching_cells;
|
||||
uint16_t cross_half_connections;
|
||||
uint16_t start_to_key[2];
|
||||
uint16_t key_to_exit[2];
|
||||
uint16_t start_to_exit[2];
|
||||
uint16_t start_to_compass[2];
|
||||
uint32_t landmark_distance_total[2];
|
||||
uint32_t artifact_distance_total[2];
|
||||
} maze32_maze_metrics_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_MAZE_OK = 0,
|
||||
MAZE32_MAZE_INVALID_ARGUMENT,
|
||||
MAZE32_MAZE_INVALID_CONFIG,
|
||||
MAZE32_MAZE_UNREACHABLE,
|
||||
MAZE32_MAZE_ASYMMETRIC,
|
||||
MAZE32_MAZE_INSUFFICIENT_CYCLES,
|
||||
MAZE32_MAZE_INSUFFICIENT_CROSSINGS,
|
||||
MAZE32_MAZE_PLACEMENT_COLLISION,
|
||||
MAZE32_MAZE_UNFAIR_PLACEMENT,
|
||||
MAZE32_MAZE_KEY_IN_INITIAL_VIEW,
|
||||
MAZE32_MAZE_GENERATION_EXHAUSTED,
|
||||
} maze32_maze_result_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_maze_result_t result;
|
||||
maze32_maze_result_t last_validation_result;
|
||||
uint8_t attempts;
|
||||
maze32_maze_metrics_t metrics;
|
||||
} maze32_maze_report_t;
|
||||
|
||||
maze32_maze_config_t maze32_maze_default_config(void);
|
||||
maze32_maze_result_t maze32_maze_generate(maze32_maze_t* maze, maze32_maze_workspace_t* workspace,
|
||||
const maze32_maze_config_t* config, uint32_t seed,
|
||||
maze32_maze_report_t* report);
|
||||
maze32_maze_result_t maze32_maze_validate(const maze32_maze_t* maze,
|
||||
maze32_maze_workspace_t* workspace,
|
||||
maze32_maze_metrics_t* metrics);
|
||||
uint16_t maze32_maze_cell_index(const maze32_maze_t* maze, maze32_cell_t cell);
|
||||
maze32_cell_t maze32_maze_rotate(const maze32_maze_t* maze, maze32_cell_t cell);
|
||||
int maze32_maze_has_passage(const maze32_maze_t* maze, maze32_cell_t cell,
|
||||
maze32_passage_t passage);
|
||||
uint64_t maze32_maze_hash(const maze32_maze_t* maze);
|
||||
const char* maze32_maze_result_name(maze32_maze_result_t result);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,716 @@
|
||||
#include "maze32_maze.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t state;
|
||||
} maze32_rng_t;
|
||||
|
||||
static uint16_t cell_count(const maze32_maze_t* maze) {
|
||||
return (uint16_t)(maze->config.width * maze->config.height);
|
||||
}
|
||||
|
||||
static uint32_t next_random(maze32_rng_t* random) {
|
||||
uint32_t value = random->state;
|
||||
value ^= value << 13U;
|
||||
value ^= value >> 17U;
|
||||
value ^= value << 5U;
|
||||
random->state = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
static uint32_t bounded_random(maze32_rng_t* random, uint32_t limit) {
|
||||
return limit == 0U ? 0U : next_random(random) % limit;
|
||||
}
|
||||
|
||||
static int config_is_valid(const maze32_maze_config_t* config) {
|
||||
if (config == NULL || config->width < 8U || config->height < 8U ||
|
||||
config->width > MAZE32_MAZE_MAX_WIDTH || config->height > MAZE32_MAZE_MAX_HEIGHT ||
|
||||
(config->width & 1U) != 0U || (config->height & 1U) != 0U ||
|
||||
config->cross_half_pairs < 2U || config->cross_half_pairs > config->height / 2U ||
|
||||
config->minimum_exit_separation > config->width + config->height - 2U ||
|
||||
config->landmark_pairs > MAZE32_MAZE_MAX_LANDMARK_PAIRS || config->artifact_pairs == 0U ||
|
||||
config->artifact_pairs > MAZE32_MAZE_MAX_ARTIFACT_PAIRS || config->maximum_attempts == 0U) {
|
||||
return 0;
|
||||
}
|
||||
return (uint32_t)config->width * config->height <= MAZE32_MAZE_MAX_CELLS;
|
||||
}
|
||||
|
||||
static int cells_equal(maze32_cell_t first, maze32_cell_t second) {
|
||||
return first.x == second.x && first.y == second.y;
|
||||
}
|
||||
|
||||
static maze32_cell_t cell_from_index(const maze32_maze_t* maze, uint16_t index) {
|
||||
maze32_cell_t cell = {
|
||||
.x = (uint16_t)(index % maze->config.width),
|
||||
.y = (uint16_t)(index / maze->config.width),
|
||||
};
|
||||
return cell;
|
||||
}
|
||||
|
||||
uint16_t maze32_maze_cell_index(const maze32_maze_t* maze, maze32_cell_t cell) {
|
||||
if (maze == NULL || cell.x >= maze->config.width || cell.y >= maze->config.height) {
|
||||
return UINT16_MAX;
|
||||
}
|
||||
return (uint16_t)(cell.y * maze->config.width + cell.x);
|
||||
}
|
||||
|
||||
maze32_cell_t maze32_maze_rotate(const maze32_maze_t* maze, maze32_cell_t cell) {
|
||||
maze32_cell_t rotated = {UINT16_MAX, UINT16_MAX};
|
||||
if (maze != NULL && cell.x < maze->config.width && cell.y < maze->config.height) {
|
||||
rotated.x = (uint16_t)(maze->config.width - 1U - cell.x);
|
||||
rotated.y = (uint16_t)(maze->config.height - 1U - cell.y);
|
||||
}
|
||||
return rotated;
|
||||
}
|
||||
|
||||
static maze32_passage_t opposite_passage(maze32_passage_t passage) {
|
||||
switch (passage) {
|
||||
case MAZE32_PASSAGE_NORTH:
|
||||
return MAZE32_PASSAGE_SOUTH;
|
||||
case MAZE32_PASSAGE_EAST:
|
||||
return MAZE32_PASSAGE_WEST;
|
||||
case MAZE32_PASSAGE_SOUTH:
|
||||
return MAZE32_PASSAGE_NORTH;
|
||||
case MAZE32_PASSAGE_WEST:
|
||||
return MAZE32_PASSAGE_EAST;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int neighbor_for(const maze32_maze_t* maze, uint16_t index, maze32_passage_t passage,
|
||||
uint16_t* neighbor) {
|
||||
maze32_cell_t cell = cell_from_index(maze, index);
|
||||
if (passage == MAZE32_PASSAGE_NORTH && cell.y > 0U) {
|
||||
cell.y--;
|
||||
} else if (passage == MAZE32_PASSAGE_EAST && cell.x + 1U < maze->config.width) {
|
||||
cell.x++;
|
||||
} else if (passage == MAZE32_PASSAGE_SOUTH && cell.y + 1U < maze->config.height) {
|
||||
cell.y++;
|
||||
} else if (passage == MAZE32_PASSAGE_WEST && cell.x > 0U) {
|
||||
cell.x--;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
*neighbor = maze32_maze_cell_index(maze, cell);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void open_edge(maze32_maze_t* maze, uint16_t first, uint16_t second,
|
||||
maze32_passage_t passage) {
|
||||
maze->passages[first] |= (uint8_t)passage;
|
||||
maze->passages[second] |= (uint8_t)opposite_passage(passage);
|
||||
}
|
||||
|
||||
static void open_symmetric_edge(maze32_maze_t* maze, uint16_t first, uint16_t second,
|
||||
maze32_passage_t passage) {
|
||||
open_edge(maze, first, second, passage);
|
||||
maze32_cell_t rotated_first = maze32_maze_rotate(maze, cell_from_index(maze, first));
|
||||
maze32_cell_t rotated_second = maze32_maze_rotate(maze, cell_from_index(maze, second));
|
||||
open_edge(maze, maze32_maze_cell_index(maze, rotated_first),
|
||||
maze32_maze_cell_index(maze, rotated_second), opposite_passage(passage));
|
||||
}
|
||||
|
||||
static void clear_visited(maze32_maze_workspace_t* workspace) {
|
||||
memset(workspace->visited, 0, sizeof(workspace->visited));
|
||||
}
|
||||
|
||||
static int is_visited(const maze32_maze_workspace_t* workspace, uint16_t index) {
|
||||
return (workspace->visited[index / 8U] & (uint8_t)(1U << (index % 8U))) != 0U;
|
||||
}
|
||||
|
||||
static void mark_visited(maze32_maze_workspace_t* workspace, uint16_t index) {
|
||||
workspace->visited[index / 8U] |= (uint8_t)(1U << (index % 8U));
|
||||
}
|
||||
|
||||
static int build_half_tree(maze32_maze_t* maze, maze32_maze_workspace_t* workspace,
|
||||
maze32_rng_t* random) {
|
||||
static const maze32_passage_t DIRECTIONS[] = {
|
||||
MAZE32_PASSAGE_NORTH,
|
||||
MAZE32_PASSAGE_EAST,
|
||||
MAZE32_PASSAGE_SOUTH,
|
||||
MAZE32_PASSAGE_WEST,
|
||||
};
|
||||
uint16_t half_width = maze->config.width / 2U;
|
||||
uint16_t target_cells = (uint16_t)(half_width * maze->config.height);
|
||||
uint16_t visited_cells = 1U;
|
||||
size_t stack_size = 1U;
|
||||
clear_visited(workspace);
|
||||
workspace->traversal[0] = 0U;
|
||||
mark_visited(workspace, 0U);
|
||||
|
||||
while (stack_size > 0U) {
|
||||
uint16_t current = workspace->traversal[stack_size - 1U];
|
||||
maze32_passage_t choices[4];
|
||||
uint16_t neighbors[4];
|
||||
size_t choice_count = 0U;
|
||||
for (size_t direction_index = 0U; direction_index < 4U; ++direction_index) {
|
||||
uint16_t neighbor = 0U;
|
||||
if (neighbor_for(maze, current, DIRECTIONS[direction_index], &neighbor)) {
|
||||
maze32_cell_t neighbor_cell = cell_from_index(maze, neighbor);
|
||||
if (neighbor_cell.x < half_width && !is_visited(workspace, neighbor)) {
|
||||
choices[choice_count] = DIRECTIONS[direction_index];
|
||||
neighbors[choice_count] = neighbor;
|
||||
choice_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (choice_count == 0U) {
|
||||
stack_size--;
|
||||
continue;
|
||||
}
|
||||
size_t selected = bounded_random(random, (uint32_t)choice_count);
|
||||
open_symmetric_edge(maze, current, neighbors[selected], choices[selected]);
|
||||
mark_visited(workspace, neighbors[selected]);
|
||||
workspace->traversal[stack_size++] = neighbors[selected];
|
||||
visited_cells++;
|
||||
}
|
||||
return visited_cells == target_cells;
|
||||
}
|
||||
|
||||
static int add_cycle_pairs(maze32_maze_t* maze, maze32_rng_t* random) {
|
||||
uint16_t half_width = maze->config.width / 2U;
|
||||
uint16_t added = 0U;
|
||||
uint32_t attempt_limit = (uint32_t)maze->config.extra_cycle_pairs * 64U + 256U;
|
||||
for (uint32_t attempt = 0U; attempt < attempt_limit && added < maze->config.extra_cycle_pairs;
|
||||
++attempt) {
|
||||
maze32_cell_t cell = {
|
||||
.x = (uint16_t)bounded_random(random, half_width),
|
||||
.y = (uint16_t)bounded_random(random, maze->config.height),
|
||||
};
|
||||
maze32_passage_t passage =
|
||||
bounded_random(random, 2U) == 0U ? MAZE32_PASSAGE_EAST : MAZE32_PASSAGE_SOUTH;
|
||||
uint16_t first = maze32_maze_cell_index(maze, cell);
|
||||
uint16_t second = 0U;
|
||||
if (!neighbor_for(maze, first, passage, &second) ||
|
||||
cell_from_index(maze, second).x >= half_width ||
|
||||
(maze->passages[first] & passage) != 0U) {
|
||||
continue;
|
||||
}
|
||||
open_symmetric_edge(maze, first, second, passage);
|
||||
added++;
|
||||
}
|
||||
return added == maze->config.extra_cycle_pairs;
|
||||
}
|
||||
|
||||
static int add_cross_half_connections(maze32_maze_t* maze, maze32_maze_workspace_t* workspace,
|
||||
maze32_rng_t* random) {
|
||||
clear_visited(workspace);
|
||||
uint16_t added_pairs = 0U;
|
||||
uint16_t half_height = maze->config.height / 2U;
|
||||
while (added_pairs < maze->config.cross_half_pairs) {
|
||||
uint16_t y = (uint16_t)bounded_random(random, half_height);
|
||||
if (is_visited(workspace, y)) {
|
||||
continue;
|
||||
}
|
||||
mark_visited(workspace, y);
|
||||
maze32_cell_t left = {(uint16_t)(maze->config.width / 2U - 1U), y};
|
||||
uint16_t first = maze32_maze_cell_index(maze, left);
|
||||
uint16_t second = 0U;
|
||||
if (!neighbor_for(maze, first, MAZE32_PASSAGE_EAST, &second)) {
|
||||
return 0;
|
||||
}
|
||||
open_symmetric_edge(maze, first, second, MAZE32_PASSAGE_EAST);
|
||||
added_pairs++;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint16_t calculate_distances(const maze32_maze_t* maze, maze32_maze_workspace_t* workspace,
|
||||
maze32_cell_t start) {
|
||||
uint16_t count = cell_count(maze);
|
||||
for (uint16_t index = 0U; index < count; ++index) {
|
||||
workspace->distances[index] = UINT16_MAX;
|
||||
}
|
||||
uint16_t start_index = maze32_maze_cell_index(maze, start);
|
||||
size_t head = 0U;
|
||||
size_t tail = 1U;
|
||||
workspace->traversal[0] = start_index;
|
||||
workspace->distances[start_index] = 0U;
|
||||
while (head < tail) {
|
||||
uint16_t current = workspace->traversal[head++];
|
||||
const maze32_passage_t directions[] = {MAZE32_PASSAGE_NORTH, MAZE32_PASSAGE_EAST,
|
||||
MAZE32_PASSAGE_SOUTH, MAZE32_PASSAGE_WEST};
|
||||
for (size_t direction_index = 0U; direction_index < 4U; ++direction_index) {
|
||||
maze32_passage_t passage = directions[direction_index];
|
||||
uint16_t neighbor = 0U;
|
||||
if ((maze->passages[current] & passage) != 0U &&
|
||||
neighbor_for(maze, current, passage, &neighbor) &&
|
||||
workspace->distances[neighbor] == UINT16_MAX) {
|
||||
workspace->distances[neighbor] = (uint16_t)(workspace->distances[current] + 1U);
|
||||
workspace->traversal[tail++] = neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (uint16_t)tail;
|
||||
}
|
||||
|
||||
static int required_cell_occupied(const maze32_maze_t* maze, maze32_cell_t cell,
|
||||
unsigned int placement_stage) {
|
||||
unsigned int pair_counts[] = {2U, 2U, 2U, 2U};
|
||||
const maze32_cell_t* required[] = {maze->starts, maze->keys, maze->exits, maze->compasses};
|
||||
for (unsigned int group = 0U; group < placement_stage && group < 4U; ++group) {
|
||||
for (unsigned int index = 0U; index < pair_counts[group]; ++index) {
|
||||
if (cells_equal(cell, required[group][index])) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
unsigned int placed_landmarks = placement_stage > 4U ? placement_stage - 4U : 0U;
|
||||
if (placed_landmarks > maze->config.landmark_pairs) {
|
||||
placed_landmarks = maze->config.landmark_pairs;
|
||||
}
|
||||
for (unsigned int pair = 0U; pair < placed_landmarks; ++pair) {
|
||||
if (cells_equal(cell, maze->landmarks[pair].cells[0]) ||
|
||||
cells_equal(cell, maze->landmarks[pair].cells[1])) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
unsigned int artifact_stage = 4U + maze->config.landmark_pairs;
|
||||
unsigned int placed_artifacts =
|
||||
placement_stage > artifact_stage ? placement_stage - artifact_stage : 0U;
|
||||
if (placed_artifacts > maze->config.artifact_pairs) {
|
||||
placed_artifacts = maze->config.artifact_pairs;
|
||||
}
|
||||
for (unsigned int pair = 0U; pair < placed_artifacts; ++pair) {
|
||||
if (cells_equal(cell, maze->artifacts[pair][0]) ||
|
||||
cells_equal(cell, maze->artifacts[pair][1])) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pair_is_available(const maze32_maze_t* maze, maze32_cell_t first,
|
||||
unsigned int placement_stage) {
|
||||
maze32_cell_t second = maze32_maze_rotate(maze, first);
|
||||
return !cells_equal(first, second) && !required_cell_occupied(maze, first, placement_stage) &&
|
||||
!required_cell_occupied(maze, second, placement_stage);
|
||||
}
|
||||
|
||||
static int choose_farthest(const maze32_maze_t* maze, maze32_maze_workspace_t* workspace,
|
||||
unsigned int placement_stage, int boundary_only,
|
||||
uint16_t maximum_distance, const maze32_cell_t* separated_from,
|
||||
uint16_t minimum_separation, maze32_cell_t* selected) {
|
||||
uint16_t best_distance = 0U;
|
||||
int found = 0;
|
||||
uint16_t count = cell_count(maze);
|
||||
for (uint16_t index = 0U; index < count; ++index) {
|
||||
maze32_cell_t cell = cell_from_index(maze, index);
|
||||
int on_boundary = cell.x == 0U || cell.y == 0U || cell.x + 1U == maze->config.width ||
|
||||
cell.y + 1U == maze->config.height;
|
||||
uint16_t separation = 0U;
|
||||
if (separated_from != NULL) {
|
||||
separation = (uint16_t)((cell.x > separated_from->x ? cell.x - separated_from->x
|
||||
: separated_from->x - cell.x) +
|
||||
(cell.y > separated_from->y ? cell.y - separated_from->y
|
||||
: separated_from->y - cell.y));
|
||||
}
|
||||
uint16_t distance = workspace->distances[index];
|
||||
if ((!boundary_only || on_boundary) && distance != UINT16_MAX &&
|
||||
distance <= maximum_distance && separation >= minimum_separation &&
|
||||
pair_is_available(maze, cell, placement_stage) &&
|
||||
(!found || distance > best_distance)) {
|
||||
*selected = cell;
|
||||
best_distance = distance;
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static int choose_random_pair(const maze32_maze_t* maze, maze32_rng_t* random,
|
||||
unsigned int placement_stage, maze32_cell_t* selected) {
|
||||
uint16_t count = cell_count(maze);
|
||||
for (uint32_t attempt = 0U; attempt < (uint32_t)count * 2U; ++attempt) {
|
||||
maze32_cell_t candidate = cell_from_index(maze, (uint16_t)bounded_random(random, count));
|
||||
if (pair_is_available(maze, candidate, placement_stage)) {
|
||||
*selected = candidate;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
for (uint16_t index = 0U; index < count; ++index) {
|
||||
maze32_cell_t candidate = cell_from_index(maze, index);
|
||||
if (pair_is_available(maze, candidate, placement_stage)) {
|
||||
*selected = candidate;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int place_objects(maze32_maze_t* maze, maze32_maze_workspace_t* workspace,
|
||||
maze32_rng_t* random) {
|
||||
maze->starts[0].x = 0U;
|
||||
maze->starts[0].y = (uint16_t)bounded_random(random, maze->config.height);
|
||||
maze->starts[1] = maze32_maze_rotate(maze, maze->starts[0]);
|
||||
|
||||
calculate_distances(maze, workspace, maze->starts[0]);
|
||||
if (!choose_farthest(maze, workspace, 1U, 0, UINT16_MAX, NULL, 0U, &maze->keys[0])) {
|
||||
return 0;
|
||||
}
|
||||
maze->keys[1] = maze32_maze_rotate(maze, maze->keys[0]);
|
||||
uint16_t start_key_distance = workspace->distances[maze32_maze_cell_index(maze, maze->keys[0])];
|
||||
|
||||
calculate_distances(maze, workspace, maze->keys[0]);
|
||||
if (!choose_farthest(maze, workspace, 2U, 1, start_key_distance, &maze->starts[0],
|
||||
maze->config.minimum_exit_separation, &maze->exits[0])) {
|
||||
return 0;
|
||||
}
|
||||
maze->exits[1] = maze32_maze_rotate(maze, maze->exits[0]);
|
||||
|
||||
if (!choose_random_pair(maze, random, 3U, &maze->compasses[0])) {
|
||||
return 0;
|
||||
}
|
||||
maze->compasses[1] = maze32_maze_rotate(maze, maze->compasses[0]);
|
||||
|
||||
for (unsigned int pair = 0U; pair < maze->config.landmark_pairs; ++pair) {
|
||||
if (!choose_random_pair(maze, random, 4U + pair, &maze->landmarks[pair].cells[0])) {
|
||||
return 0;
|
||||
}
|
||||
maze->landmarks[pair].cells[1] = maze32_maze_rotate(maze, maze->landmarks[pair].cells[0]);
|
||||
maze->landmarks[pair].kind = (uint8_t)(pair % 4U + 1U);
|
||||
}
|
||||
for (unsigned int pair = 0U; pair < maze->config.artifact_pairs; ++pair) {
|
||||
if (!choose_random_pair(maze, random, 4U + maze->config.landmark_pairs + pair,
|
||||
&maze->artifacts[pair][0])) {
|
||||
return 0;
|
||||
}
|
||||
maze->artifacts[pair][1] = maze32_maze_rotate(maze, maze->artifacts[pair][0]);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static maze32_maze_result_t generate_candidate(maze32_maze_t* maze,
|
||||
maze32_maze_workspace_t* workspace,
|
||||
const maze32_maze_config_t* config,
|
||||
uint32_t requested_seed, uint32_t generation_seed,
|
||||
uint8_t attempt) {
|
||||
memset(maze, 0, sizeof(*maze));
|
||||
maze->config = *config;
|
||||
maze->requested_seed = requested_seed;
|
||||
maze->generation_seed = generation_seed;
|
||||
maze->generation_attempts = attempt;
|
||||
maze32_rng_t random = {.state = generation_seed == 0U ? 0x6d2b79f5U : generation_seed};
|
||||
if (!build_half_tree(maze, workspace, &random) || !add_cycle_pairs(maze, &random) ||
|
||||
!add_cross_half_connections(maze, workspace, &random) ||
|
||||
!place_objects(maze, workspace, &random)) {
|
||||
return MAZE32_MAZE_PLACEMENT_COLLISION;
|
||||
}
|
||||
return MAZE32_MAZE_OK;
|
||||
}
|
||||
|
||||
static int validate_passages(const maze32_maze_t* maze, maze32_maze_metrics_t* metrics) {
|
||||
uint16_t count = cell_count(maze);
|
||||
for (uint16_t index = 0U; index < count; ++index) {
|
||||
if ((maze->passages[index] & 0xf0U) != 0U) {
|
||||
return 0;
|
||||
}
|
||||
unsigned int degree = 0U;
|
||||
const maze32_passage_t directions[] = {MAZE32_PASSAGE_NORTH, MAZE32_PASSAGE_EAST,
|
||||
MAZE32_PASSAGE_SOUTH, MAZE32_PASSAGE_WEST};
|
||||
for (size_t direction_index = 0U; direction_index < 4U; ++direction_index) {
|
||||
maze32_passage_t passage = directions[direction_index];
|
||||
uint16_t neighbor = 0U;
|
||||
if ((maze->passages[index] & passage) == 0U) {
|
||||
continue;
|
||||
}
|
||||
if (!neighbor_for(maze, index, passage, &neighbor) ||
|
||||
(maze->passages[neighbor] & opposite_passage(passage)) == 0U) {
|
||||
return 0;
|
||||
}
|
||||
degree++;
|
||||
if (passage == MAZE32_PASSAGE_EAST || passage == MAZE32_PASSAGE_SOUTH) {
|
||||
metrics->passage_edges++;
|
||||
}
|
||||
}
|
||||
if (degree == 1U) {
|
||||
metrics->dead_end_count++;
|
||||
} else if (degree >= 3U) {
|
||||
metrics->branching_cells++;
|
||||
}
|
||||
maze32_cell_t cell = cell_from_index(maze, index);
|
||||
maze32_cell_t rotated = maze32_maze_rotate(maze, cell);
|
||||
uint16_t rotated_index = maze32_maze_cell_index(maze, rotated);
|
||||
uint8_t expected = 0U;
|
||||
if ((maze->passages[index] & MAZE32_PASSAGE_NORTH) != 0U) {
|
||||
expected |= MAZE32_PASSAGE_SOUTH;
|
||||
}
|
||||
if ((maze->passages[index] & MAZE32_PASSAGE_EAST) != 0U) {
|
||||
expected |= MAZE32_PASSAGE_WEST;
|
||||
}
|
||||
if ((maze->passages[index] & MAZE32_PASSAGE_SOUTH) != 0U) {
|
||||
expected |= MAZE32_PASSAGE_NORTH;
|
||||
}
|
||||
if ((maze->passages[index] & MAZE32_PASSAGE_WEST) != 0U) {
|
||||
expected |= MAZE32_PASSAGE_EAST;
|
||||
}
|
||||
if (maze->passages[rotated_index] != expected) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int object_pair_is_valid(const maze32_maze_t* maze, const maze32_cell_t pair[2]) {
|
||||
return maze32_maze_cell_index(maze, pair[0]) != UINT16_MAX &&
|
||||
maze32_maze_cell_index(maze, pair[1]) != UINT16_MAX &&
|
||||
cells_equal(maze32_maze_rotate(maze, pair[0]), pair[1]);
|
||||
}
|
||||
|
||||
static maze32_maze_result_t validate_objects(const maze32_maze_t* maze,
|
||||
maze32_maze_workspace_t* workspace,
|
||||
maze32_maze_metrics_t* metrics) {
|
||||
maze32_cell_t
|
||||
occupied[8U + MAZE32_MAZE_MAX_LANDMARK_PAIRS * 2U + MAZE32_MAZE_MAX_ARTIFACT_PAIRS * 2U];
|
||||
size_t occupied_count = 0U;
|
||||
const maze32_cell_t* required[] = {maze->starts, maze->keys, maze->exits, maze->compasses};
|
||||
for (size_t group = 0U; group < 4U; ++group) {
|
||||
if (!object_pair_is_valid(maze, required[group])) {
|
||||
return MAZE32_MAZE_UNFAIR_PLACEMENT;
|
||||
}
|
||||
for (size_t index = 0U; index < 2U; ++index) {
|
||||
for (size_t previous = 0U; previous < occupied_count; ++previous) {
|
||||
if (cells_equal(required[group][index], occupied[previous])) {
|
||||
return MAZE32_MAZE_PLACEMENT_COLLISION;
|
||||
}
|
||||
}
|
||||
occupied[occupied_count++] = required[group][index];
|
||||
}
|
||||
}
|
||||
for (size_t pair = 0U; pair < maze->config.landmark_pairs; ++pair) {
|
||||
if (!object_pair_is_valid(maze, maze->landmarks[pair].cells)) {
|
||||
return MAZE32_MAZE_UNFAIR_PLACEMENT;
|
||||
}
|
||||
for (size_t index = 0U; index < 2U; ++index) {
|
||||
for (size_t previous = 0U; previous < occupied_count; ++previous) {
|
||||
if (cells_equal(maze->landmarks[pair].cells[index], occupied[previous])) {
|
||||
return MAZE32_MAZE_PLACEMENT_COLLISION;
|
||||
}
|
||||
}
|
||||
occupied[occupied_count++] = maze->landmarks[pair].cells[index];
|
||||
}
|
||||
}
|
||||
for (size_t pair = 0U; pair < maze->config.artifact_pairs; ++pair) {
|
||||
if (!object_pair_is_valid(maze, maze->artifacts[pair])) {
|
||||
return MAZE32_MAZE_UNFAIR_PLACEMENT;
|
||||
}
|
||||
for (size_t index = 0U; index < 2U; ++index) {
|
||||
for (size_t previous = 0U; previous < occupied_count; ++previous) {
|
||||
if (cells_equal(maze->artifacts[pair][index], occupied[previous])) {
|
||||
return MAZE32_MAZE_PLACEMENT_COLLISION;
|
||||
}
|
||||
}
|
||||
occupied[occupied_count++] = maze->artifacts[pair][index];
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t player = 0U; player < 2U; ++player) {
|
||||
maze32_cell_t start = maze->starts[player];
|
||||
maze32_cell_t exit = maze->exits[player];
|
||||
int start_on_boundary = start.x == 0U || start.y == 0U ||
|
||||
start.x + 1U == maze->config.width ||
|
||||
start.y + 1U == maze->config.height;
|
||||
int exit_on_boundary = exit.x == 0U || exit.y == 0U || exit.x + 1U == maze->config.width ||
|
||||
exit.y + 1U == maze->config.height;
|
||||
uint16_t exit_separation =
|
||||
(uint16_t)((start.x > exit.x ? start.x - exit.x : exit.x - start.x) +
|
||||
(start.y > exit.y ? start.y - exit.y : exit.y - start.y));
|
||||
if (!start_on_boundary || !exit_on_boundary ||
|
||||
exit_separation < maze->config.minimum_exit_separation) {
|
||||
return MAZE32_MAZE_UNFAIR_PLACEMENT;
|
||||
}
|
||||
}
|
||||
|
||||
metrics->reachable_cells = calculate_distances(maze, workspace, maze->starts[0]);
|
||||
if (metrics->reachable_cells != cell_count(maze)) {
|
||||
return MAZE32_MAZE_UNREACHABLE;
|
||||
}
|
||||
metrics->start_to_key[0] = workspace->distances[maze32_maze_cell_index(maze, maze->keys[0])];
|
||||
metrics->start_to_exit[0] = workspace->distances[maze32_maze_cell_index(maze, maze->exits[0])];
|
||||
metrics->start_to_compass[0] =
|
||||
workspace->distances[maze32_maze_cell_index(maze, maze->compasses[0])];
|
||||
for (size_t pair = 0U; pair < maze->config.landmark_pairs; ++pair) {
|
||||
metrics->landmark_distance_total[0] +=
|
||||
workspace->distances[maze32_maze_cell_index(maze, maze->landmarks[pair].cells[0])];
|
||||
}
|
||||
for (size_t pair = 0U; pair < maze->config.artifact_pairs; ++pair) {
|
||||
metrics->artifact_distance_total[0] +=
|
||||
workspace->distances[maze32_maze_cell_index(maze, maze->artifacts[pair][0])];
|
||||
}
|
||||
for (size_t player = 0U; player < 2U; ++player) {
|
||||
int x_distance = maze->starts[player].x > maze->keys[player].x
|
||||
? maze->starts[player].x - maze->keys[player].x
|
||||
: maze->keys[player].x - maze->starts[player].x;
|
||||
int y_distance = maze->starts[player].y > maze->keys[player].y
|
||||
? maze->starts[player].y - maze->keys[player].y
|
||||
: maze->keys[player].y - maze->starts[player].y;
|
||||
if (x_distance <= maze->config.initial_view_radius &&
|
||||
y_distance <= maze->config.initial_view_radius) {
|
||||
return MAZE32_MAZE_KEY_IN_INITIAL_VIEW;
|
||||
}
|
||||
}
|
||||
calculate_distances(maze, workspace, maze->keys[0]);
|
||||
metrics->key_to_exit[0] = workspace->distances[maze32_maze_cell_index(maze, maze->exits[0])];
|
||||
metrics->start_to_key[1] = metrics->start_to_key[0];
|
||||
metrics->key_to_exit[1] = metrics->key_to_exit[0];
|
||||
metrics->start_to_exit[1] = metrics->start_to_exit[0];
|
||||
metrics->start_to_compass[1] = metrics->start_to_compass[0];
|
||||
metrics->landmark_distance_total[1] = metrics->landmark_distance_total[0];
|
||||
metrics->artifact_distance_total[1] = metrics->artifact_distance_total[0];
|
||||
if (metrics->start_to_key[0] < metrics->key_to_exit[0]) {
|
||||
return MAZE32_MAZE_UNFAIR_PLACEMENT;
|
||||
}
|
||||
return MAZE32_MAZE_OK;
|
||||
}
|
||||
|
||||
maze32_maze_config_t maze32_maze_default_config(void) {
|
||||
maze32_maze_config_t config = {
|
||||
.width = MAZE32_MAZE_DEFAULT_WIDTH,
|
||||
.height = MAZE32_MAZE_DEFAULT_HEIGHT,
|
||||
.initial_view_radius = 3U,
|
||||
.cross_half_pairs = 3U,
|
||||
.extra_cycle_pairs = 64U,
|
||||
.minimum_exit_separation = 64U,
|
||||
.landmark_pairs = 8U,
|
||||
.artifact_pairs = 4U,
|
||||
.maximum_attempts = 8U,
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
maze32_maze_result_t maze32_maze_validate(const maze32_maze_t* maze,
|
||||
maze32_maze_workspace_t* workspace,
|
||||
maze32_maze_metrics_t* metrics) {
|
||||
if (maze == NULL || workspace == NULL || metrics == NULL) {
|
||||
return MAZE32_MAZE_INVALID_ARGUMENT;
|
||||
}
|
||||
memset(metrics, 0, sizeof(*metrics));
|
||||
if (!config_is_valid(&maze->config)) {
|
||||
return MAZE32_MAZE_INVALID_CONFIG;
|
||||
}
|
||||
if (!validate_passages(maze, metrics)) {
|
||||
return MAZE32_MAZE_ASYMMETRIC;
|
||||
}
|
||||
maze32_maze_result_t object_result = validate_objects(maze, workspace, metrics);
|
||||
if (object_result != MAZE32_MAZE_OK) {
|
||||
return object_result;
|
||||
}
|
||||
metrics->cycle_count = (uint16_t)(metrics->passage_edges - metrics->reachable_cells + 1U);
|
||||
if (metrics->cycle_count == 0U || metrics->dead_end_count == 0U) {
|
||||
return MAZE32_MAZE_INSUFFICIENT_CYCLES;
|
||||
}
|
||||
uint16_t seam_x = maze->config.width / 2U - 1U;
|
||||
for (uint16_t y = 0U; y < maze->config.height; ++y) {
|
||||
maze32_cell_t cell = {seam_x, y};
|
||||
if (maze32_maze_has_passage(maze, cell, MAZE32_PASSAGE_EAST)) {
|
||||
metrics->cross_half_connections++;
|
||||
}
|
||||
}
|
||||
if (metrics->cross_half_connections < 4U) {
|
||||
return MAZE32_MAZE_INSUFFICIENT_CROSSINGS;
|
||||
}
|
||||
return MAZE32_MAZE_OK;
|
||||
}
|
||||
|
||||
maze32_maze_result_t maze32_maze_generate(maze32_maze_t* maze, maze32_maze_workspace_t* workspace,
|
||||
const maze32_maze_config_t* config, uint32_t seed,
|
||||
maze32_maze_report_t* report) {
|
||||
if (maze == NULL || workspace == NULL || config == NULL || report == NULL) {
|
||||
return MAZE32_MAZE_INVALID_ARGUMENT;
|
||||
}
|
||||
memset(report, 0, sizeof(*report));
|
||||
if (!config_is_valid(config)) {
|
||||
report->result = MAZE32_MAZE_INVALID_CONFIG;
|
||||
return report->result;
|
||||
}
|
||||
maze32_maze_result_t last_result = MAZE32_MAZE_GENERATION_EXHAUSTED;
|
||||
uint32_t generation_seed = seed;
|
||||
for (uint8_t attempt = 1U; attempt <= config->maximum_attempts; ++attempt) {
|
||||
generation_seed += 0x9e3779b9U;
|
||||
last_result = generate_candidate(maze, workspace, config, seed, generation_seed, attempt);
|
||||
if (last_result == MAZE32_MAZE_OK) {
|
||||
last_result = maze32_maze_validate(maze, workspace, &report->metrics);
|
||||
}
|
||||
report->attempts = attempt;
|
||||
report->last_validation_result = last_result;
|
||||
if (last_result == MAZE32_MAZE_OK) {
|
||||
report->result = MAZE32_MAZE_OK;
|
||||
return MAZE32_MAZE_OK;
|
||||
}
|
||||
}
|
||||
report->result = MAZE32_MAZE_GENERATION_EXHAUSTED;
|
||||
return report->result;
|
||||
}
|
||||
|
||||
int maze32_maze_has_passage(const maze32_maze_t* maze, maze32_cell_t cell,
|
||||
maze32_passage_t passage) {
|
||||
uint16_t index = maze32_maze_cell_index(maze, cell);
|
||||
return index != UINT16_MAX && (maze->passages[index] & passage) != 0U;
|
||||
}
|
||||
|
||||
static void hash_bytes(uint64_t* hash, const void* data, size_t size) {
|
||||
const uint8_t* bytes = data;
|
||||
for (size_t index = 0U; index < size; ++index) {
|
||||
*hash ^= bytes[index];
|
||||
*hash *= UINT64_C(1099511628211);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t maze32_maze_hash(const maze32_maze_t* maze) {
|
||||
if (maze == NULL || !config_is_valid(&maze->config)) {
|
||||
return 0U;
|
||||
}
|
||||
uint64_t hash = UINT64_C(1469598103934665603);
|
||||
hash_bytes(&hash, &maze->config.width, sizeof(maze->config.width));
|
||||
hash_bytes(&hash, &maze->config.height, sizeof(maze->config.height));
|
||||
hash_bytes(&hash, &maze->config.initial_view_radius, sizeof(maze->config.initial_view_radius));
|
||||
hash_bytes(&hash, &maze->config.cross_half_pairs, sizeof(maze->config.cross_half_pairs));
|
||||
hash_bytes(&hash, &maze->config.extra_cycle_pairs, sizeof(maze->config.extra_cycle_pairs));
|
||||
hash_bytes(&hash, &maze->config.minimum_exit_separation,
|
||||
sizeof(maze->config.minimum_exit_separation));
|
||||
hash_bytes(&hash, &maze->config.landmark_pairs, sizeof(maze->config.landmark_pairs));
|
||||
hash_bytes(&hash, &maze->config.artifact_pairs, sizeof(maze->config.artifact_pairs));
|
||||
hash_bytes(&hash, &maze->config.maximum_attempts, sizeof(maze->config.maximum_attempts));
|
||||
hash_bytes(&hash, &maze->requested_seed, sizeof(maze->requested_seed));
|
||||
hash_bytes(&hash, &maze->generation_seed, sizeof(maze->generation_seed));
|
||||
hash_bytes(&hash, maze->passages, cell_count(maze));
|
||||
hash_bytes(&hash, maze->starts, sizeof(maze->starts));
|
||||
hash_bytes(&hash, maze->keys, sizeof(maze->keys));
|
||||
hash_bytes(&hash, maze->exits, sizeof(maze->exits));
|
||||
hash_bytes(&hash, maze->compasses, sizeof(maze->compasses));
|
||||
hash_bytes(&hash, maze->landmarks, maze->config.landmark_pairs * sizeof(maze->landmarks[0]));
|
||||
hash_bytes(&hash, maze->artifacts, maze->config.artifact_pairs * sizeof(maze->artifacts[0]));
|
||||
return hash;
|
||||
}
|
||||
|
||||
const char* maze32_maze_result_name(maze32_maze_result_t result) {
|
||||
switch (result) {
|
||||
case MAZE32_MAZE_OK:
|
||||
return "ok";
|
||||
case MAZE32_MAZE_INVALID_ARGUMENT:
|
||||
return "invalid_argument";
|
||||
case MAZE32_MAZE_INVALID_CONFIG:
|
||||
return "invalid_config";
|
||||
case MAZE32_MAZE_UNREACHABLE:
|
||||
return "unreachable";
|
||||
case MAZE32_MAZE_ASYMMETRIC:
|
||||
return "asymmetric";
|
||||
case MAZE32_MAZE_INSUFFICIENT_CYCLES:
|
||||
return "insufficient_cycles";
|
||||
case MAZE32_MAZE_INSUFFICIENT_CROSSINGS:
|
||||
return "insufficient_crossings";
|
||||
case MAZE32_MAZE_PLACEMENT_COLLISION:
|
||||
return "placement_collision";
|
||||
case MAZE32_MAZE_UNFAIR_PLACEMENT:
|
||||
return "unfair_placement";
|
||||
case MAZE32_MAZE_KEY_IN_INITIAL_VIEW:
|
||||
return "key_in_initial_view";
|
||||
case MAZE32_MAZE_GENERATION_EXHAUSTED:
|
||||
return "generation_exhausted";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user