feat(maze): add deterministic symmetric maze generation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user