#include "esp_app_desc.h" #include "esp_err.h" #include "esp_event.h" #include "esp_heap_caps.h" #include "esp_littlefs.h" #include "esp_log.h" #include "esp_netif.h" #include "esp_timer.h" #include #ifdef MAZE32_MILESTONE_003_DEVICE_TEST #include "esp_system.h" #include #endif #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "maze32_config_store.h" #include "maze32_game.h" #include "maze32_health.h" #include "maze32_maze.h" #include "maze32_network.h" #include "maze32_web.h" static const char* TAG = "maze32"; static maze32_health_t health; static httpd_handle_t web_server; static maze32_network_t network; static maze32_gateway_t gateway; #ifdef MAZE32_MILESTONE_007_DEVICE_TEST static maze32_maze_t mechanics_test_maze; static maze32_maze_workspace_t mechanics_test_workspace; static maze32_game_t mechanics_test_game; static maze32_view_t mechanics_test_view; static char mechanics_test_payload[MAZE32_GAME_VIEW_CHUNK_BYTES]; static int entry_move_for(const maze32_maze_t* maze, maze32_cell_t target, maze32_cell_t* starting_position, maze32_game_direction_t* direction) { if (target.y > 0U && maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_NORTH)) { *starting_position = (maze32_cell_t){target.x, (uint16_t)(target.y - 1U)}; *direction = MAZE32_GAME_DIRECTION_SOUTH; return 1; } if (target.x + 1U < maze->config.width && maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_EAST)) { *starting_position = (maze32_cell_t){(uint16_t)(target.x + 1U), target.y}; *direction = MAZE32_GAME_DIRECTION_WEST; return 1; } if (target.y + 1U < maze->config.height && maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_SOUTH)) { *starting_position = (maze32_cell_t){target.x, (uint16_t)(target.y + 1U)}; *direction = MAZE32_GAME_DIRECTION_NORTH; return 1; } if (target.x > 0U && maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_WEST)) { *starting_position = (maze32_cell_t){(uint16_t)(target.x - 1U), target.y}; *direction = MAZE32_GAME_DIRECTION_EAST; return 1; } return 0; } static maze32_game_direction_t outward_direction(const maze32_maze_t* maze, maze32_cell_t exit_cell) { if (exit_cell.x == 0U) { return MAZE32_GAME_DIRECTION_WEST; } if (exit_cell.x + 1U == maze->config.width) { return MAZE32_GAME_DIRECTION_EAST; } if (exit_cell.y == 0U) { return MAZE32_GAME_DIRECTION_NORTH; } return MAZE32_GAME_DIRECTION_SOUTH; } static esp_err_t run_mechanics_device_test(void) { maze32_maze_config_t maze_config = maze32_maze_default_config(); maze32_maze_report_t maze_report; if (maze32_maze_generate(&mechanics_test_maze, &mechanics_test_workspace, &maze_config, UINT32_C(0x5a320007), &maze_report) != MAZE32_MAZE_OK) { ESP_LOGE(TAG, "M007 physical mechanics test could not generate its maze"); return ESP_FAIL; } maze32_game_config_t game_config = maze32_game_default_config(); game_config.movement_cooldown_ms = 0U; size_t free_before = heap_caps_get_free_size(MALLOC_CAP_8BIT); int64_t started_us = esp_timer_get_time(); if (maze32_game_init(&mechanics_test_game, &mechanics_test_maze, &game_config) != MAZE32_GAME_RESULT_ACCEPTED) { return ESP_FAIL; } maze32_game_set_phase(&mechanics_test_game, MAZE32_GAME_PHASE_ACTIVE); maze32_cell_t entry_position; maze32_game_direction_t entry_direction; uint32_t command_sequence = 0U; if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.artifacts[0][0], &entry_position, &entry_direction)) { return ESP_FAIL; } mechanics_test_game.players[0].position = entry_position; maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction, 100U, &command_sequence); maze32_game_command_result_t artifact_result = maze32_game_process_next(&mechanics_test_game); if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.keys[0], &entry_position, &entry_direction)) { return ESP_FAIL; } mechanics_test_game.players[0].position = entry_position; maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction, 300U, &command_sequence); maze32_game_command_result_t key_result = maze32_game_process_next(&mechanics_test_game); if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.compasses[0], &entry_position, &entry_direction)) { return ESP_FAIL; } mechanics_test_game.players[0].position = entry_position; maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction, 500U, &command_sequence); maze32_game_command_result_t compass_result = maze32_game_process_next(&mechanics_test_game); if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.exits[0], &entry_position, &entry_direction)) { return ESP_FAIL; } mechanics_test_game.players[0].position = entry_position; maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction, 700U, &command_sequence); maze32_game_command_result_t exit_result = maze32_game_process_next(&mechanics_test_game); maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, outward_direction(&mechanics_test_maze, mechanics_test_maze.exits[0]), 900U, &command_sequence); maze32_game_command_result_t outward_result = maze32_game_process_next(&mechanics_test_game); int finalized = maze32_game_advance_time(&mechanics_test_game, 1101U); if (artifact_result.result != MAZE32_GAME_RESULT_ACCEPTED || !artifact_result.activated_artifact || key_result.result != MAZE32_GAME_RESULT_ACCEPTED || !key_result.picked_key || key_result.key != 0U || compass_result.compass_hint == MAZE32_GAME_COMPASS_NONE || !exit_result.opened_exit || !outward_result.outward_exit || !finalized || mechanics_test_game.final_result != MAZE32_GAME_FINAL_PLAYER_ONE || maze32_game_project_view(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, &mechanics_test_view) != MAZE32_GAME_RESULT_ACCEPTED) { ESP_LOGE(TAG, "M007 physical mechanics transitions failed"); return ESP_FAIL; } size_t player_chunks = maze32_game_view_chunk_count(&mechanics_test_view); size_t payload_bytes = 0U; if (maze32_game_serialize_view_chunk(&mechanics_test_view, 0U, mechanics_test_payload, sizeof(mechanics_test_payload), &payload_bytes) != MAZE32_GAME_RESULT_ACCEPTED || maze32_game_project_view(&mechanics_test_game, MAZE32_GAME_ROLE_OBSERVER, &mechanics_test_view) != MAZE32_GAME_RESULT_ACCEPTED) { return ESP_FAIL; } 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(mechanics_test_game) + sizeof(mechanics_test_view) + sizeof(mechanics_test_payload); ESP_LOGI(TAG, "M007 events: artifact=%u, key=%u, hint=%u, opened=%u, outward=%u, result=%u, " "sequence=%" PRIu32, artifact_result.artifact, key_result.key, compass_result.compass_hint, exit_result.opened_exit, outward_result.outward_exit, mechanics_test_game.final_result, mechanics_test_game.state_sequence); ESP_LOGI(TAG, "M007 views: player_chunks=%u, first_chunk=%u bytes, observer_cells=%u, " "observer_objects=%u, observer_chunks=%u", (unsigned int)player_chunks, (unsigned int)payload_bytes, mechanics_test_view.cell_count, mechanics_test_view.object_count, (unsigned int)maze32_game_view_chunk_count(&mechanics_test_view)); ESP_LOGI(TAG, "M007 budget: fixed=%u/%u bytes, runtime=%" PRIu64 "/%u ms, heap_delta=%d", (unsigned int)fixed_bytes, (unsigned int)MAZE32_GAME_RAM_BUDGET_BYTES, elapsed_us / 1000U, (unsigned int)MAZE32_GAME_RUNTIME_BUDGET_MS, (int)(free_before - free_after)); if (fixed_bytes > MAZE32_GAME_RAM_BUDGET_BYTES || elapsed_us > (uint64_t)MAZE32_GAME_RUNTIME_BUDGET_MS * 1000U || free_before != free_after) { ESP_LOGE(TAG, "M007 physical mechanics budget failed"); return ESP_FAIL; } ESP_LOGI(TAG, "M007 physical mechanics test: PASS"); return ESP_OK; } #endif #ifdef MAZE32_MILESTONE_006_DEVICE_TEST static maze32_maze_t game_test_maze; static maze32_maze_workspace_t game_test_workspace; static maze32_game_t device_test_game; static maze32_view_t device_test_view; static char device_test_payload[MAZE32_GAME_VIEW_CHUNK_BYTES]; static maze32_game_direction_t first_open_direction(const maze32_maze_t* maze, maze32_cell_t cell) { static const maze32_game_direction_t DIRECTIONS[] = { MAZE32_GAME_DIRECTION_NORTH, MAZE32_GAME_DIRECTION_EAST, MAZE32_GAME_DIRECTION_SOUTH, MAZE32_GAME_DIRECTION_WEST, }; static const maze32_passage_t PASSAGES[] = { MAZE32_PASSAGE_NORTH, MAZE32_PASSAGE_EAST, MAZE32_PASSAGE_SOUTH, MAZE32_PASSAGE_WEST, }; for (size_t index = 0U; index < sizeof(DIRECTIONS) / sizeof(DIRECTIONS[0]); ++index) { if (maze32_maze_has_passage(maze, cell, PASSAGES[index])) { return DIRECTIONS[index]; } } return (maze32_game_direction_t)UINT8_MAX; } static esp_err_t run_game_device_test(void) { maze32_maze_config_t maze_config = maze32_maze_default_config(); maze32_maze_report_t maze_report; if (maze32_maze_generate(&game_test_maze, &game_test_workspace, &maze_config, UINT32_C(0x5a320006), &maze_report) != MAZE32_MAZE_OK) { ESP_LOGE(TAG, "M006 physical game test could not generate its maze"); return ESP_FAIL; } maze32_game_config_t game_config = maze32_game_default_config(); size_t free_before = heap_caps_get_free_size(MALLOC_CAP_8BIT); int64_t started_us = esp_timer_get_time(); if (maze32_game_init(&device_test_game, &game_test_maze, &game_config) != MAZE32_GAME_RESULT_ACCEPTED) { return ESP_FAIL; } maze32_game_set_phase(&device_test_game, MAZE32_GAME_PHASE_ACTIVE); maze32_game_direction_t direction = first_open_direction(&game_test_maze, device_test_game.players[0].position); uint32_t command_sequence = 0U; if (direction > MAZE32_GAME_DIRECTION_WEST || maze32_game_enqueue_move(&device_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, direction, 0U, &command_sequence) != MAZE32_GAME_RESULT_ACCEPTED) { return ESP_FAIL; } maze32_game_command_result_t command_result = maze32_game_process_next(&device_test_game); if (command_result.result != MAZE32_GAME_RESULT_ACCEPTED || command_result.state_sequence != 1U || maze32_game_project_view(&device_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, &device_test_view) != MAZE32_GAME_RESULT_ACCEPTED) { return ESP_FAIL; } uint16_t player_cells = device_test_view.cell_count; uint8_t player_objects = device_test_view.object_count; size_t player_chunks = maze32_game_view_chunk_count(&device_test_view); size_t payload_bytes = 0U; if (maze32_game_serialize_view_chunk(&device_test_view, 0U, device_test_payload, sizeof(device_test_payload), &payload_bytes) != MAZE32_GAME_RESULT_ACCEPTED) { return ESP_FAIL; } if (maze32_game_project_view(&device_test_game, MAZE32_GAME_ROLE_OBSERVER, &device_test_view) != MAZE32_GAME_RESULT_ACCEPTED) { return ESP_FAIL; } 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_game) + sizeof(device_test_view) + sizeof(device_test_payload); ESP_LOGI(TAG, "M006 command: command=%" PRIu32 ", state=%" PRIu32 ", result=%s, position=%u,%u", command_sequence, command_result.state_sequence, maze32_game_result_name(command_result.result), device_test_game.players[0].position.x, device_test_game.players[0].position.y); ESP_LOGI(TAG, "M006 views: player_cells=%u, player_objects=%u, player_chunks=%u, " "observer_cells=%u, observer_objects=%u, observer_chunks=%u, first_chunk=%u bytes", player_cells, player_objects, (unsigned int)player_chunks, device_test_view.cell_count, device_test_view.object_count, (unsigned int)maze32_game_view_chunk_count(&device_test_view), (unsigned int)payload_bytes); ESP_LOGI(TAG, "M006 budget: fixed=%u/%u bytes, runtime=%" PRIu64 "/%u ms, heap_delta=%d", (unsigned int)fixed_bytes, (unsigned int)MAZE32_GAME_RAM_BUDGET_BYTES, elapsed_us / 1000U, (unsigned int)MAZE32_GAME_RUNTIME_BUDGET_MS, (int)(free_before - free_after)); if (device_test_view.cell_count != MAZE32_MAZE_DEFAULT_WIDTH * MAZE32_MAZE_DEFAULT_HEIGHT || fixed_bytes > MAZE32_GAME_RAM_BUDGET_BYTES || elapsed_us > (uint64_t)MAZE32_GAME_RUNTIME_BUDGET_MS * 1000U || free_before != free_after) { ESP_LOGE(TAG, "M006 physical game test failed"); return ESP_FAIL; } ESP_LOGI(TAG, "M006 physical game test: PASS"); return ESP_OK; } #endif #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 static int device_test_has_persisted_credentials; static esp_err_t run_device_test_before_network(void) { const maze32_wifi_credentials_t expected = { .ssid = "maze32-unavailable-test", .password = "test-only-password", }; maze32_wifi_credentials_t loaded = {0}; esp_err_t result = maze32_config_store_load(&loaded); if (result == ESP_ERR_NOT_FOUND) { result = maze32_config_store_save(&expected); if (result == ESP_OK) { ESP_LOGI(TAG, "M003 device test: credentials saved; restarting"); esp_restart(); } return result; } if (result != ESP_OK || strcmp(loaded.ssid, expected.ssid) != 0 || strcmp(loaded.password, expected.password) != 0) { ESP_LOGE(TAG, "M003 device test refused to overwrite unexpected NVS credentials"); return ESP_ERR_INVALID_STATE; } device_test_has_persisted_credentials = 1; ESP_LOGI(TAG, "M003 device test: credentials survived restart"); return ESP_OK; } static esp_err_t finish_device_test(void) { if (!device_test_has_persisted_credentials) { return ESP_ERR_INVALID_STATE; } esp_err_t result = maze32_config_store_clear(); if (result == ESP_OK) { ESP_LOGI(TAG, "M003 device test: failed station returned to fallback; credentials cleared"); } return result; } #endif static int prepare_for_network_reconfiguration(void* context) { const maze32_gateway_t* current_gateway = context; if (!maze32_gateway_can_reconfigure(current_gateway)) { ESP_LOGW(TAG, "Network reconfiguration rejected while lobby is active"); return -1; } ESP_LOGW(TAG, "Runtime accepted network reconfiguration while lobby is waiting"); return 0; } static esp_err_t mount_littlefs(void) { const esp_vfs_littlefs_conf_t configuration = { .base_path = "/littlefs", .partition_label = "littlefs", .format_if_mount_failed = false, .dont_mount = false, }; esp_err_t result = esp_vfs_littlefs_register(&configuration); if (result != ESP_OK) { return result; } size_t total_bytes = 0; size_t used_bytes = 0; result = esp_littlefs_info(configuration.partition_label, &total_bytes, &used_bytes); if (result != ESP_OK) { esp_vfs_littlefs_unregister(configuration.partition_label); return result; } ESP_LOGI(TAG, "LittleFS mounted: total=%u bytes, used=%u bytes", (unsigned int)total_bytes, (unsigned int)used_bytes); return ESP_OK; } void app_main(void) { const esp_app_desc_t* application = esp_app_get_description(); 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 #ifdef MAZE32_MILESTONE_006_DEVICE_TEST if (run_game_device_test() != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)ESP_FAIL); return; } #endif #ifdef MAZE32_MILESTONE_007_DEVICE_TEST if (run_mechanics_device_test() != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)ESP_FAIL); return; } #endif vTaskDelay(pdMS_TO_TICKS(1500)); ESP_LOGI(TAG, "maze32 firmware version %s starting", application->version); esp_err_t result = mount_littlefs(); if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); ESP_LOGE(TAG, "LittleFS mount failed: %s", esp_err_to_name(result)); return; } maze32_health_set(&health, MAZE32_HEALTH_FILESYSTEM_READY, 0); result = maze32_config_store_init(); if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); ESP_LOGE(TAG, "Persistent configuration initialization failed: %s", esp_err_to_name(result)); return; } #ifdef MAZE32_MILESTONE_003_DEVICE_TEST result = run_device_test_before_network(); if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); return; } #endif result = esp_netif_init(); if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); ESP_LOGE(TAG, "TCP/IP stack initialization failed: %s", esp_err_to_name(result)); return; } result = esp_event_loop_create_default(); if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); ESP_LOGE(TAG, "Default event loop initialization failed: %s", esp_err_to_name(result)); return; } result = maze32_gateway_init(&gateway); if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); ESP_LOGE(TAG, "WebSocket gateway initialization failed: %s", esp_err_to_name(result)); return; } result = maze32_network_init(&network, prepare_for_network_reconfiguration, &gateway); if (result == ESP_OK) { result = maze32_network_start(&network); } if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); ESP_LOGE(TAG, "Network initialization failed: %s", esp_err_to_name(result)); return; } #ifdef MAZE32_MILESTONE_003_DEVICE_TEST result = finish_device_test(); if (result != ESP_OK) { maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); return; } #endif result = maze32_web_start(&web_server, &health, &gateway); if (result == ESP_OK) { result = maze32_gateway_register(web_server, &gateway); } if (result == ESP_OK) { result = maze32_provisioning_register(web_server, &network); } if (result != ESP_OK) { if (web_server != NULL) { httpd_stop(web_server); web_server = NULL; } maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result); ESP_LOGE(TAG, "HTTP server startup failed: %s", esp_err_to_name(result)); return; } maze32_health_set(&health, MAZE32_HEALTH_SERVING, 0); ESP_LOGI(TAG, "Health state: %s", maze32_health_state_name(health.state)); }