feat(maze): add deterministic symmetric maze generation
This commit is contained in:
@@ -362,11 +362,11 @@ observers, and an owner-controlled lobby.
|
|||||||
snapshots. The observer protocol exposes no mutating command, while start
|
snapshots. The observer protocol exposes no mutating command, while start
|
||||||
authorization is resolved from the server-owned session and player slot.
|
authorization is resolved from the server-owned session and player slot.
|
||||||
- These results provide concrete host and physical-device evidence for all six
|
- These results provide concrete host and physical-device evidence for all six
|
||||||
acceptance criteria. Milestone 005 remains `NOT_STARTED`.
|
acceptance criteria.
|
||||||
|
|
||||||
## Milestone 005 — Symmetric maze generation and validation
|
## Milestone 005 — Symmetric maze generation and validation
|
||||||
|
|
||||||
**Status:** `NOT_STARTED`
|
**Status:** `COMPLETE`
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
@@ -400,9 +400,66 @@ object placement and bounded memory use.
|
|||||||
|
|
||||||
### Evidence
|
### Evidence
|
||||||
|
|
||||||
Completion evidence is not yet available because this milestone has not
|
- On 2026-09-04, `/home/sasa/.platformio/penv/bin/pio project config` exited
|
||||||
started. Required evidence includes deterministic seed-test results, validator
|
successfully and resolved the production, Milestone 003 device-test, and
|
||||||
statistics, memory measurements, and representative generated maps.
|
isolated `milestone-005-device-test` environments from the locked
|
||||||
|
`espressif32@7.0.1`, ESP-IDF, 4 MB Flash, root partition, and root sdkconfig
|
||||||
|
baseline.
|
||||||
|
- On 2026-09-04, `./scripts/run_host_tests.sh` reported all preceding tests as
|
||||||
|
`PASS`, `maze bounded regeneration failure: PASS (3/3 attempts)`, and
|
||||||
|
`maze seed sweep: PASS (1024 seeds, combined hash=a264b7d133f8bad4)`.
|
||||||
|
Generating every default `128 x 64` seed twice produced identical hashes and
|
||||||
|
valid results. All 1,024 maps contained 8,192 reachable cells, exactly 133
|
||||||
|
cycles, six cross-half connections, 704–854 dead ends, and symmetric object
|
||||||
|
and route metrics. Start-to-key distances were 416–1,442, key-to-exit
|
||||||
|
distances were 346–1,244, and start-to-exit distances were 102–992; every
|
||||||
|
tested candidate succeeded on its first bounded attempt and key-search route
|
||||||
|
distance was never shorter than the following exit-search route distance.
|
||||||
|
- The same host suite reported a maximum 5.127 ms generation time and
|
||||||
|
`maze fixed memory: PASS (42332/65536 bytes, no heap API)`. The portable
|
||||||
|
component uses one byte of passage flags per cell plus fixed bit-set,
|
||||||
|
traversal, distance, and placement arrays. A separate build with
|
||||||
|
`-fsanitize=address,undefined` and `ASAN_OPTIONS=detect_leaks=0` repeated the
|
||||||
|
1,024-seed suite without an address or undefined-behavior finding; leak
|
||||||
|
detection was disabled because the sandbox executes under `ptrace`.
|
||||||
|
- Validator corruption tests independently rejected a broken reciprocal or
|
||||||
|
rotational passage and a colliding key placement. The sweep also verified
|
||||||
|
boundary starts and exits, keys outside each owner's Chebyshev-radius-three
|
||||||
|
initial view, corresponding distances for keys, exits, compasses, all eight
|
||||||
|
landmark pairs, and all four vision-artifact pairs. The forced impossible
|
||||||
|
candidate stopped after the configured three attempts and returned a
|
||||||
|
diagnostic failure instead of retrying indefinitely.
|
||||||
|
- The deterministic small-map diagnostic reported representative map hash
|
||||||
|
`7396c6426181da3e` and printed a complete `16 x 8` ASCII maze with walls,
|
||||||
|
opposite starts, symmetric keys and exits, compasses, landmarks, and four
|
||||||
|
vision artifacts. `docs/MAZE_GENERATION.md` records the construction,
|
||||||
|
validation, configuration, reproducibility, and resource contracts.
|
||||||
|
- `/home/sasa/.platformio/penv/bin/pio run -e esp32-c6-devkitm-1` completed
|
||||||
|
with `[SUCCESS]`. PlatformIO reported 47,508 of 327,680 RAM bytes and
|
||||||
|
1,005,274 of 2,097,152 application bytes in use; the production upload's
|
||||||
|
independent partition check reported
|
||||||
|
`firmware.bin uses 1005904/2097152 bytes in partition factory`.
|
||||||
|
`/home/sasa/.platformio/penv/bin/pio run -e esp32-c6-devkitm-1 --target
|
||||||
|
buildfs` also completed with `[SUCCESS]` and reported
|
||||||
|
`littlefs.bin uses 2031616/2031616 bytes in partition littlefs`.
|
||||||
|
- The isolated physical-board image was built and hash-verified with
|
||||||
|
`/home/sasa/.platformio/penv/bin/pio run -e milestone-005-device-test
|
||||||
|
--target upload --upload-port /dev/ttyACM0`. The upload identified the target
|
||||||
|
as an ESP32-C6FH4 with 4 MB embedded Flash. On target, requested seed
|
||||||
|
`1513226245` generated in one attempt with hash `485280a746e8900c` and logged
|
||||||
|
`cells=8192, reachable=8192, edges=8324, cycles=133, crossings=6,
|
||||||
|
dead_ends=778, branches=1024`. Paired route metrics were equal:
|
||||||
|
`start_key=804/804`, `key_exit=773/773`, `start_exit=191/191`,
|
||||||
|
`compass=223/223`, `landmarks=3099/3099`, and `artifacts=1085/1085`.
|
||||||
|
- The same physical run logged
|
||||||
|
`fixed=42332/65536 bytes, runtime=183/250 ms, heap_delta=0` followed by
|
||||||
|
`M005 physical maze test: PASS`. Production firmware was restored and
|
||||||
|
hash-verified afterward; a live `GET /health` over the configured station
|
||||||
|
network returned
|
||||||
|
`{"status":"serving","healthy":true,"version":"0.5.0","errorCode":0}`.
|
||||||
|
- These deterministic, validator, resource, representative-map, build, and
|
||||||
|
physical-board results provide concrete evidence for all six acceptance
|
||||||
|
criteria. Milestone 006 remains `NOT_STARTED`.
|
||||||
|
|
||||||
## Milestone 006 — Authoritative movement and visibility core
|
## Milestone 006 — Authoritative movement and visibility core
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Maze generation and validation
|
||||||
|
|
||||||
|
Milestone 005 provides a portable, deterministic maze component. It does not
|
||||||
|
implement movement, visibility projection, inventory, or match behavior from
|
||||||
|
later milestones.
|
||||||
|
|
||||||
|
## Representation and limits
|
||||||
|
|
||||||
|
- Dimensions are runtime configuration values. Supported maps are even-width,
|
||||||
|
even-height rectangles from `8 x 8` through `128 x 64` and contain at most
|
||||||
|
8,192 cells.
|
||||||
|
- Each cell uses one byte of four-direction passage flags. A fixed bit set,
|
||||||
|
traversal queue, and distance array form the reusable generation workspace.
|
||||||
|
- Starts, keys, boundary exits, compass cells, landmarks, and vision artifacts
|
||||||
|
use bounded fixed arrays. No generation or validation path calls a heap API.
|
||||||
|
- The maze plus workspace budget is 64 KiB. The default `128 x 64`
|
||||||
|
configuration targets generation within 250 ms on the ESP32-C6.
|
||||||
|
- Candidate regeneration is capped by `maximum_attempts`; failure returns a
|
||||||
|
diagnostic result instead of retrying indefinitely.
|
||||||
|
|
||||||
|
## Deterministic construction
|
||||||
|
|
||||||
|
The generator builds a seeded randomized depth-first tree in one conceptual
|
||||||
|
half and rotates every opened edge by 180 degrees into the other half. It then
|
||||||
|
adds a configured number of symmetric cycle edges and symmetric pairs of seam
|
||||||
|
connections. This construction retains dead ends while guaranteeing cycles,
|
||||||
|
alternative routes, and several ways to cross between halves once validation
|
||||||
|
succeeds.
|
||||||
|
|
||||||
|
The requested seed and the derived candidate seed are recorded in the maze.
|
||||||
|
Using the same configuration and requested seed therefore reproduces the same
|
||||||
|
topology, placements, metrics, and hash.
|
||||||
|
|
||||||
|
## Placement and validation
|
||||||
|
|
||||||
|
Player 2's start and every required object are the exact rotational counterpart
|
||||||
|
of Player 1's placement. Starts and exits are boundary cells, and the default
|
||||||
|
requires at least 64 cells of Manhattan separation between each start and its
|
||||||
|
exit. Keys are selected outside the owners' initial Chebyshev-radius-three
|
||||||
|
views. The default places eight landmark pairs and four vision-artifact pairs,
|
||||||
|
with no placement collisions.
|
||||||
|
|
||||||
|
Validation independently checks reciprocal passages, rotational symmetry,
|
||||||
|
full reachability, edge and cycle counts, dead ends, half crossings, placement
|
||||||
|
bounds and collisions, initial key visibility, and corresponding route
|
||||||
|
distances for keys, exits, compasses, every landmark, and every artifact.
|
||||||
|
Reports expose these topology and route metrics for diagnostics and later
|
||||||
|
playtesting; geometric distance alone is not treated as proof of search
|
||||||
|
difficulty.
|
||||||
@@ -42,3 +42,9 @@ extends = env:esp32-c6-devkitm-1
|
|||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c6-devkitm-1.build_flags}
|
${env:esp32-c6-devkitm-1.build_flags}
|
||||||
-DMAZE32_MILESTONE_003_DEVICE_TEST=1
|
-DMAZE32_MILESTONE_003_DEVICE_TEST=1
|
||||||
|
|
||||||
|
[env:milestone-005-device-test]
|
||||||
|
extends = env:esp32-c6-devkitm-1
|
||||||
|
build_flags =
|
||||||
|
${env:esp32-c6-devkitm-1.build_flags}
|
||||||
|
-DMAZE32_MILESTONE_005_DEVICE_TEST=1
|
||||||
|
|||||||
@@ -50,4 +50,12 @@ cc -std=c11 -Wall -Wextra -Werror -pedantic \
|
|||||||
-o "$test_directory/test_lobby_protocol"
|
-o "$test_directory/test_lobby_protocol"
|
||||||
|
|
||||||
"$test_directory/test_lobby_protocol"
|
"$test_directory/test_lobby_protocol"
|
||||||
|
|
||||||
|
cc -std=c11 -Wall -Wextra -Werror -pedantic \
|
||||||
|
-I"$project_directory/components/maze32_maze/include" \
|
||||||
|
"$project_directory/components/maze32_maze/maze32_maze.c" \
|
||||||
|
"$project_directory/test/host/test_maze_generation.c" \
|
||||||
|
-o "$test_directory/test_maze_generation"
|
||||||
|
|
||||||
|
"$test_directory/test_maze_generation"
|
||||||
python3 "$project_directory/scripts/verify_web_assets.py"
|
python3 "$project_directory/scripts/verify_web_assets.py"
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.c"
|
SRCS "main.c"
|
||||||
REQUIRES esp_event esp_netif maze32_config maze32_network maze32_platform maze32_web
|
REQUIRES esp_event esp_netif esp_timer heap maze32_config maze32_maze maze32_network maze32_platform maze32_web
|
||||||
)
|
)
|
||||||
|
|||||||
+58
@@ -1,9 +1,12 @@
|
|||||||
#include "esp_app_desc.h"
|
#include "esp_app_desc.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_littlefs.h"
|
#include "esp_littlefs.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include <inttypes.h>
|
||||||
#ifdef MAZE32_MILESTONE_003_DEVICE_TEST
|
#ifdef MAZE32_MILESTONE_003_DEVICE_TEST
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -12,6 +15,7 @@
|
|||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "maze32_config_store.h"
|
#include "maze32_config_store.h"
|
||||||
#include "maze32_health.h"
|
#include "maze32_health.h"
|
||||||
|
#include "maze32_maze.h"
|
||||||
#include "maze32_network.h"
|
#include "maze32_network.h"
|
||||||
#include "maze32_web.h"
|
#include "maze32_web.h"
|
||||||
|
|
||||||
@@ -21,6 +25,53 @@ static httpd_handle_t web_server;
|
|||||||
static maze32_network_t network;
|
static maze32_network_t network;
|
||||||
static maze32_gateway_t gateway;
|
static maze32_gateway_t gateway;
|
||||||
|
|
||||||
|
#ifdef MAZE32_MILESTONE_005_DEVICE_TEST
|
||||||
|
static maze32_maze_t device_test_maze;
|
||||||
|
static maze32_maze_workspace_t device_test_workspace;
|
||||||
|
|
||||||
|
static esp_err_t run_maze_device_test(void) {
|
||||||
|
maze32_maze_config_t config = maze32_maze_default_config();
|
||||||
|
maze32_maze_report_t report;
|
||||||
|
size_t free_before = heap_caps_get_free_size(MALLOC_CAP_8BIT);
|
||||||
|
int64_t started_us = esp_timer_get_time();
|
||||||
|
maze32_maze_result_t result = maze32_maze_generate(&device_test_maze, &device_test_workspace,
|
||||||
|
&config, UINT32_C(0x5a320005), &report);
|
||||||
|
uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
|
||||||
|
size_t free_after = heap_caps_get_free_size(MALLOC_CAP_8BIT);
|
||||||
|
size_t fixed_bytes = sizeof(device_test_maze) + sizeof(device_test_workspace);
|
||||||
|
ESP_LOGI(TAG, "M005 maze: result=%s, seed=%" PRIu32 ", attempts=%u, hash=%016" PRIx64,
|
||||||
|
maze32_maze_result_name(result), device_test_maze.requested_seed, report.attempts,
|
||||||
|
maze32_maze_hash(&device_test_maze));
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"M005 topology: cells=%u, reachable=%u, edges=%u, cycles=%u, crossings=%u, "
|
||||||
|
"dead_ends=%u, branches=%u",
|
||||||
|
(unsigned int)(config.width * config.height), report.metrics.reachable_cells,
|
||||||
|
report.metrics.passage_edges, report.metrics.cycle_count,
|
||||||
|
report.metrics.cross_half_connections, report.metrics.dead_end_count,
|
||||||
|
report.metrics.branching_cells);
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"M005 fairness: start_key=%u/%u, key_exit=%u/%u, start_exit=%u/%u, "
|
||||||
|
"compass=%u/%u, landmarks=%" PRIu32 "/%" PRIu32 ", artifacts=%" PRIu32 "/%" PRIu32,
|
||||||
|
report.metrics.start_to_key[0], report.metrics.start_to_key[1],
|
||||||
|
report.metrics.key_to_exit[0], report.metrics.key_to_exit[1],
|
||||||
|
report.metrics.start_to_exit[0], report.metrics.start_to_exit[1],
|
||||||
|
report.metrics.start_to_compass[0], report.metrics.start_to_compass[1],
|
||||||
|
report.metrics.landmark_distance_total[0], report.metrics.landmark_distance_total[1],
|
||||||
|
report.metrics.artifact_distance_total[0], report.metrics.artifact_distance_total[1]);
|
||||||
|
ESP_LOGI(TAG, "M005 budget: fixed=%u/%u bytes, runtime=%" PRIu64 "/%u ms, heap_delta=%d",
|
||||||
|
(unsigned int)fixed_bytes, (unsigned int)MAZE32_MAZE_RAM_BUDGET_BYTES,
|
||||||
|
elapsed_us / 1000U, (unsigned int)MAZE32_MAZE_RUNTIME_BUDGET_MS,
|
||||||
|
(int)(free_before - free_after));
|
||||||
|
if (result != MAZE32_MAZE_OK || fixed_bytes > MAZE32_MAZE_RAM_BUDGET_BYTES ||
|
||||||
|
elapsed_us > (uint64_t)MAZE32_MAZE_RUNTIME_BUDGET_MS * 1000U || free_before != free_after) {
|
||||||
|
ESP_LOGE(TAG, "M005 physical maze test failed");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "M005 physical maze test: PASS");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef MAZE32_MILESTONE_003_DEVICE_TEST
|
#ifdef MAZE32_MILESTONE_003_DEVICE_TEST
|
||||||
static int device_test_has_persisted_credentials;
|
static int device_test_has_persisted_credentials;
|
||||||
|
|
||||||
@@ -101,6 +152,13 @@ void app_main(void) {
|
|||||||
const esp_app_desc_t* application = esp_app_get_description();
|
const esp_app_desc_t* application = esp_app_get_description();
|
||||||
maze32_health_init(&health, application->version);
|
maze32_health_init(&health, application->version);
|
||||||
|
|
||||||
|
#ifdef MAZE32_MILESTONE_005_DEVICE_TEST
|
||||||
|
if (run_maze_device_test() != ESP_OK) {
|
||||||
|
maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)ESP_FAIL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||||
ESP_LOGI(TAG, "maze32 firmware version %s starting", application->version);
|
ESP_LOGI(TAG, "maze32 firmware version %s starting", application->version);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
0.4.0
|
0.5.0
|
||||||
|
|||||||
Reference in New Issue
Block a user