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
+62 -5
View File
@@ -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
+4
View File
@@ -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
+716
View File
@@ -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";
}
}
+49
View File
@@ -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.
+6
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+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;
}
+1 -1
View File
@@ -1 +1 @@
0.4.0 0.5.0