feat(maze): add deterministic symmetric maze generation

This commit is contained in:
2026-09-04 02:43:16 +03:00
parent 996bda9567
commit 5d780def9d
11 changed files with 1263 additions and 7 deletions
+234
View File
@@ -0,0 +1,234 @@
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "maze32_maze.h"
#define SEED_COUNT 1024U
static maze32_maze_t maze;
static maze32_maze_t repeated_maze;
static maze32_maze_t tampered_maze;
static maze32_maze_workspace_t workspace;
static char object_at(const maze32_maze_t* current_maze, maze32_cell_t cell) {
const char labels[] = {'1', '2', 'k', 'K', 'e', 'E', 'c', 'C'};
const maze32_cell_t* required[] = {current_maze->starts, current_maze->keys,
current_maze->exits, current_maze->compasses};
for (size_t group = 0U; group < 4U; ++group) {
for (size_t player = 0U; player < 2U; ++player) {
if (required[group][player].x == cell.x && required[group][player].y == cell.y) {
return labels[group * 2U + player];
}
}
}
for (size_t pair = 0U; pair < current_maze->config.landmark_pairs; ++pair) {
for (size_t side = 0U; side < 2U; ++side) {
if (current_maze->landmarks[pair].cells[side].x == cell.x &&
current_maze->landmarks[pair].cells[side].y == cell.y) {
return 'L';
}
}
}
for (size_t pair = 0U; pair < current_maze->config.artifact_pairs; ++pair) {
for (size_t side = 0U; side < 2U; ++side) {
if (current_maze->artifacts[pair][side].x == cell.x &&
current_maze->artifacts[pair][side].y == cell.y) {
return 'V';
}
}
}
return ' ';
}
static void print_representative_map(const maze32_maze_t* current_maze) {
puts("representative 16x8 map (1/2 starts, k/K keys, e/E exits, c/C compasses):");
for (uint16_t y = 0U; y < current_maze->config.height; ++y) {
putchar('+');
for (uint16_t x = 0U; x < current_maze->config.width; ++x) {
maze32_cell_t cell = {x, y};
fputs(maze32_maze_has_passage(current_maze, cell, MAZE32_PASSAGE_NORTH) ? " +" : "--+",
stdout);
}
putchar('\n');
putchar('|');
for (uint16_t x = 0U; x < current_maze->config.width; ++x) {
maze32_cell_t cell = {x, y};
printf("%c%c", object_at(current_maze, cell),
maze32_maze_has_passage(current_maze, cell, MAZE32_PASSAGE_EAST) ? ' ' : '|');
}
putchar('\n');
}
for (uint16_t x = 0U; x < current_maze->config.width; ++x) {
fputs("+--", stdout);
}
puts("+");
}
static void test_invalid_configuration(void) {
maze32_maze_config_t config = maze32_maze_default_config();
maze32_maze_report_t report;
config.width = 127U;
assert(maze32_maze_generate(&maze, &workspace, &config, 1U, &report) ==
MAZE32_MAZE_INVALID_CONFIG);
assert(report.result == MAZE32_MAZE_INVALID_CONFIG);
}
static void test_bounded_regeneration_failure(void) {
maze32_maze_config_t config = maze32_maze_default_config();
maze32_maze_report_t report;
config.width = 8U;
config.height = 8U;
config.extra_cycle_pairs = 64U;
config.minimum_exit_separation = 4U;
config.maximum_attempts = 3U;
assert(maze32_maze_generate(&maze, &workspace, &config, 7U, &report) ==
MAZE32_MAZE_GENERATION_EXHAUSTED);
assert(report.attempts == config.maximum_attempts);
assert(report.last_validation_result != MAZE32_MAZE_OK);
puts("maze bounded regeneration failure: PASS (3/3 attempts)");
}
static void test_large_seed_set(void) {
maze32_maze_config_t config = maze32_maze_default_config();
uint16_t minimum_cycles = UINT16_MAX;
uint16_t maximum_cycles = 0U;
uint16_t minimum_dead_ends = UINT16_MAX;
uint16_t maximum_dead_ends = 0U;
uint16_t minimum_start_key = UINT16_MAX;
uint16_t maximum_start_key = 0U;
uint16_t minimum_key_exit = UINT16_MAX;
uint16_t maximum_key_exit = 0U;
uint16_t minimum_start_exit = UINT16_MAX;
uint16_t maximum_start_exit = 0U;
uint8_t maximum_attempts = 0U;
clock_t maximum_ticks = 0;
uint64_t combined_hash = 0U;
for (uint32_t seed = 0U; seed < SEED_COUNT; ++seed) {
maze32_maze_report_t report;
clock_t started = clock();
assert(maze32_maze_generate(&maze, &workspace, &config, seed, &report) == MAZE32_MAZE_OK);
clock_t elapsed = clock() - started;
if (elapsed > maximum_ticks) {
maximum_ticks = elapsed;
}
assert(report.result == MAZE32_MAZE_OK);
assert(report.attempts >= 1U && report.attempts <= config.maximum_attempts);
if (report.attempts > maximum_attempts) {
maximum_attempts = report.attempts;
}
assert(report.metrics.reachable_cells == MAZE32_MAZE_MAX_CELLS);
assert(report.metrics.cycle_count > 0U);
assert(report.metrics.cross_half_connections >= 4U);
assert(report.metrics.dead_end_count > 0U);
assert(report.metrics.start_to_key[0] == report.metrics.start_to_key[1]);
assert(report.metrics.key_to_exit[0] == report.metrics.key_to_exit[1]);
assert(report.metrics.start_to_exit[0] == report.metrics.start_to_exit[1]);
assert(report.metrics.start_to_compass[0] == report.metrics.start_to_compass[1]);
assert(report.metrics.landmark_distance_total[0] ==
report.metrics.landmark_distance_total[1]);
assert(report.metrics.artifact_distance_total[0] ==
report.metrics.artifact_distance_total[1]);
assert(report.metrics.start_to_key[0] >= report.metrics.key_to_exit[0]);
uint64_t first_hash = maze32_maze_hash(&maze);
maze32_maze_report_t repeated_report;
assert(maze32_maze_generate(&repeated_maze, &workspace, &config, seed, &repeated_report) ==
MAZE32_MAZE_OK);
assert(first_hash == maze32_maze_hash(&repeated_maze));
combined_hash ^= first_hash + seed * UINT64_C(0x9e3779b97f4a7c15);
if (report.metrics.cycle_count < minimum_cycles) {
minimum_cycles = report.metrics.cycle_count;
}
if (report.metrics.cycle_count > maximum_cycles) {
maximum_cycles = report.metrics.cycle_count;
}
if (report.metrics.dead_end_count < minimum_dead_ends) {
minimum_dead_ends = report.metrics.dead_end_count;
}
if (report.metrics.dead_end_count > maximum_dead_ends) {
maximum_dead_ends = report.metrics.dead_end_count;
}
if (report.metrics.start_to_key[0] < minimum_start_key) {
minimum_start_key = report.metrics.start_to_key[0];
}
if (report.metrics.start_to_key[0] > maximum_start_key) {
maximum_start_key = report.metrics.start_to_key[0];
}
if (report.metrics.key_to_exit[0] < minimum_key_exit) {
minimum_key_exit = report.metrics.key_to_exit[0];
}
if (report.metrics.key_to_exit[0] > maximum_key_exit) {
maximum_key_exit = report.metrics.key_to_exit[0];
}
if (report.metrics.start_to_exit[0] < minimum_start_exit) {
minimum_start_exit = report.metrics.start_to_exit[0];
}
if (report.metrics.start_to_exit[0] > maximum_start_exit) {
maximum_start_exit = report.metrics.start_to_exit[0];
}
}
double maximum_ms = (double)maximum_ticks * 1000.0 / CLOCKS_PER_SEC;
printf("maze seed sweep: PASS (%u seeds, combined hash=%016" PRIx64 ")\n", SEED_COUNT,
combined_hash);
printf("maze metrics: cycles=%u..%u, dead ends=%u..%u, start-key=%u..%u, "
"key-exit=%u..%u, start-exit=%u..%u, max attempts=%u\n",
minimum_cycles, maximum_cycles, minimum_dead_ends, maximum_dead_ends, minimum_start_key,
maximum_start_key, minimum_key_exit, maximum_key_exit, minimum_start_exit,
maximum_start_exit, maximum_attempts);
printf("maze host runtime: maximum %.3f ms per generated map\n", maximum_ms);
}
static void test_validator_rejects_corruption(void) {
maze32_maze_config_t config = maze32_maze_default_config();
maze32_maze_report_t report;
maze32_maze_metrics_t metrics;
assert(maze32_maze_generate(&maze, &workspace, &config, 42U, &report) == MAZE32_MAZE_OK);
tampered_maze = maze;
tampered_maze.passages[0] ^= MAZE32_PASSAGE_EAST;
assert(maze32_maze_validate(&tampered_maze, &workspace, &metrics) == MAZE32_MAZE_ASYMMETRIC);
tampered_maze = maze;
tampered_maze.keys[0] = tampered_maze.starts[0];
tampered_maze.keys[1] = tampered_maze.starts[1];
assert(maze32_maze_validate(&tampered_maze, &workspace, &metrics) ==
MAZE32_MAZE_PLACEMENT_COLLISION);
puts("maze corruption rejection: PASS");
}
static void test_memory_and_representative_map(void) {
size_t fixed_bytes = sizeof(maze32_maze_t) + sizeof(maze32_maze_workspace_t);
assert(fixed_bytes <= MAZE32_MAZE_RAM_BUDGET_BYTES);
printf("maze fixed memory: PASS (%zu/%u bytes, no heap API)\n", fixed_bytes,
MAZE32_MAZE_RAM_BUDGET_BYTES);
maze32_maze_config_t config = maze32_maze_default_config();
config.width = 16U;
config.height = 8U;
config.cross_half_pairs = 2U;
config.extra_cycle_pairs = 4U;
config.minimum_exit_separation = 4U;
config.landmark_pairs = 2U;
config.artifact_pairs = 2U;
maze32_maze_report_t report;
assert(maze32_maze_generate(&maze, &workspace, &config, UINT32_C(0x5a17), &report) ==
MAZE32_MAZE_OK);
printf("representative map hash: %016" PRIx64 "\n", maze32_maze_hash(&maze));
print_representative_map(&maze);
}
int main(void) {
test_invalid_configuration();
test_bounded_regeneration_failure();
test_large_seed_set();
test_validator_rejects_corruption();
test_memory_and_representative_map();
puts("maze generation tests: PASS");
return 0;
}