diff --git a/PLANS.md b/PLANS.md index 4227796..dfca0f3 100644 --- a/PLANS.md +++ b/PLANS.md @@ -392,7 +392,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record, ## Milestone 006 — Establish the bounded production architecture -**Status:** `READY` +**Status:** `DONE` **Depends on:** Milestone 005 ### Objective @@ -421,11 +421,22 @@ Create the production firmware and test structure without implementing game beha If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 007 from `BLOCKED` to `READY`. +### Execution record + +- Date: 2026-08-28 +- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2. +- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; `esp_littlefs` 1.20.4. +- Result: PASS. +- Evidence: Added bounded production headers for configuration, types, application ownership, game engine, fleet generation, bot scheduling, sessions, presenter, statistics, transport, diagnostics, and deterministic interfaces. Added a fixed 16-command FIFO and a host test. HTTP diagnostics now include largest free block, connected clients, reset reason, and rejected oversized input. Network-facing code has no access to `application_t`; only the application queue may deliver commands to the future game owner. +- Measurements: Firmware build passed with 38,212 / 327,680 B RAM (11.7%) and 1,000,730 / 2,097,152 B flash (47.7%), within the Milestone 005 architecture gate of 1,080,000 B firmware and 220,000 B remaining heap. `make -C test/host run` passed the host command-queue test. +- Issues or deviations: No game behavior was implemented; that remains Milestone 007. No PSRAM, exceptions, RTTI, or heap-owning core containers were added. +- Next action: Milestone 007 is ready. Do not start it unless explicitly requested. + --- ## Milestone 007 — Implement and exhaustively test the game domain core -**Status:** `BLOCKED` +**Status:** `READY` **Depends on:** Milestone 006 ### Objective diff --git a/include/app_config.h b/include/app_config.h new file mode 100644 index 0000000..34ebb7d --- /dev/null +++ b/include/app_config.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +enum { + kBoardWidth = 10, + kBoardHeight = 10, + kBoardCellCount = kBoardWidth * kBoardHeight, + kFleetShipCount = 10, + kPlayerCapacity = 2, + kSpectatorCapacity = 8, + kSessionCapacity = kPlayerCapacity + kSpectatorCapacity, + kCommandQueueCapacity = 16, + kStateMessageCapacity = 512, + kRequestBodyCapacity = 192, + kSessionTokenBytes = 16, + kDisplayNameBytes = 80, +}; diff --git a/include/app_interfaces.h b/include/app_interfaces.h new file mode 100644 index 0000000..8d13529 --- /dev/null +++ b/include/app_interfaces.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include + +typedef struct { uint64_t (*now_ms)(void *context); void *context; } clock_t; +typedef struct { uint32_t (*next_u32)(void *context); void *context; } random_source_t; +typedef struct { bool (*schedule_after_ms)(void *context, uint32_t delay_ms); void *context; } scheduler_t; +typedef struct { bool (*send)(void *context, int client_id, const char *data, size_t length); void *context; } transport_t; diff --git a/include/application.h b/include/application.h new file mode 100644 index 0000000..5070ccd --- /dev/null +++ b/include/application.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "command_queue.h" + +/* The application task is the sole owner of game_state_t mutations. */ +typedef struct { command_queue_t command_queue; game_state_t state; } application_t; + +bool application_enqueue(application_t *application, const app_command_t *command); +bool application_take_next_command(application_t *application, app_command_t *command); diff --git a/include/bot_player.h b/include/bot_player.h new file mode 100644 index 0000000..658d595 --- /dev/null +++ b/include/bot_player.h @@ -0,0 +1,5 @@ +#pragma once + +#include "app_interfaces.h" + +typedef struct { random_source_t random; scheduler_t scheduler; } bot_player_t; diff --git a/include/command_queue.h b/include/command_queue.h new file mode 100644 index 0000000..5614f91 --- /dev/null +++ b/include/command_queue.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include "app_config.h" +#include "game_types.h" + +typedef uint8_t command_type_t; +enum { COMMAND_CONFIG, COMMAND_START, COMMAND_SHOT, COMMAND_REMATCH, COMMAND_ABORT }; +typedef struct { command_type_t type; uint8_t session_index; uint32_t game_id; uint32_t version; coordinate_t coordinate; game_mode_t mode; } app_command_t; +typedef struct { app_command_t entries[kCommandQueueCapacity]; uint8_t head; uint8_t tail; uint8_t count; } command_queue_t; + +bool command_queue_push(command_queue_t *queue, const app_command_t *command); +bool command_queue_pop(command_queue_t *queue, app_command_t *command); +_Static_assert(sizeof(command_queue_t) <= 320, "command queue grew beyond fixed budget"); diff --git a/include/diagnostics.h b/include/diagnostics.h new file mode 100644 index 0000000..aa3bc8a --- /dev/null +++ b/include/diagnostics.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +typedef struct { + uint64_t uptime_ms; + uint32_t free_heap_bytes; + uint32_t minimum_free_heap_bytes; + uint32_t largest_free_block_bytes; + uint16_t connected_clients; + uint16_t rejected_oversized_input; + int reset_reason; +} diagnostics_t; diff --git a/include/fleet_generator.h b/include/fleet_generator.h new file mode 100644 index 0000000..ce47392 --- /dev/null +++ b/include/fleet_generator.h @@ -0,0 +1,6 @@ +#pragma once + +#include "app_interfaces.h" +#include "game_types.h" + +typedef struct { random_source_t random; } fleet_generator_t; diff --git a/include/game_engine.h b/include/game_engine.h new file mode 100644 index 0000000..e9a58ef --- /dev/null +++ b/include/game_engine.h @@ -0,0 +1,6 @@ +#pragma once + +#include "game_types.h" + +/* Rule implementation begins in Milestone 007. */ +typedef struct { game_state_t state; } game_engine_t; diff --git a/include/game_types.h b/include/game_types.h new file mode 100644 index 0000000..0637a3c --- /dev/null +++ b/include/game_types.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "app_config.h" + +typedef uint8_t cell_t; +enum { CELL_UNKNOWN, CELL_WATER, CELL_SHIP, CELL_MISS, CELL_HIT }; +typedef uint8_t phase_t; +enum { PHASE_LOBBY, PHASE_PREPARING, PHASE_IN_PROGRESS, PHASE_FINISHED, PHASE_REMATCH_WAIT }; +typedef uint8_t role_t; +enum { ROLE_PLAYER_1, ROLE_PLAYER_2, ROLE_SPECTATOR }; +typedef uint8_t game_mode_t; +enum { MODE_HUMAN, MODE_BOT }; + +typedef struct { uint8_t x; uint8_t y; } coordinate_t; +typedef struct { uint8_t x; uint8_t y; uint8_t length; uint8_t hits; bool horizontal; } ship_t; +typedef struct { cell_t cells[kBoardCellCount]; ship_t ships[kFleetShipCount]; uint8_t ships_alive; } board_t; +typedef struct { uint32_t game_id; uint32_t version; phase_t phase; game_mode_t mode; uint8_t current_player; board_t boards[kPlayerCapacity]; } game_state_t; + +_Static_assert(kBoardCellCount == 100, "board contract changed"); +_Static_assert(kFleetShipCount == 10, "fleet contract changed"); +_Static_assert(kSessionCapacity == 10, "session contract changed"); +_Static_assert(sizeof(coordinate_t) == 2, "coordinate must remain compact"); +_Static_assert(sizeof(ship_t) <= 6, "ship grew beyond fixed budget"); +_Static_assert(sizeof(board_t) <= 164, "board grew beyond fixed budget"); diff --git a/include/session_manager.h b/include/session_manager.h new file mode 100644 index 0000000..317783c --- /dev/null +++ b/include/session_manager.h @@ -0,0 +1,7 @@ +#pragma once + +#include "app_config.h" +#include "game_types.h" + +typedef struct { role_t role; bool occupied; uint8_t token[kSessionTokenBytes]; char name[kDisplayNameBytes + 1]; } session_t; +typedef struct { session_t entries[kSessionCapacity]; } session_manager_t; diff --git a/include/state_presenter.h b/include/state_presenter.h new file mode 100644 index 0000000..d47dd7e --- /dev/null +++ b/include/state_presenter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "game_types.h" + +bool state_presenter_write(const game_state_t *state, role_t viewer, char *output, size_t output_size, size_t *written); diff --git a/include/statistics.h b/include/statistics.h new file mode 100644 index 0000000..4ee5d16 --- /dev/null +++ b/include/statistics.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +typedef struct { uint16_t shots; uint16_t hits; uint16_t misses; uint8_t ships_sunk; } match_statistics_t; +typedef struct { uint16_t games; uint16_t wins; uint16_t losses; uint32_t shots; uint32_t hits; } cumulative_statistics_t; diff --git a/include/transport.h b/include/transport.h new file mode 100644 index 0000000..46c6e4a --- /dev/null +++ b/include/transport.h @@ -0,0 +1,5 @@ +#pragma once + +#include "app_interfaces.h" + +typedef struct { transport_t transport; } transport_service_t; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a95dd1e..ba7218a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,7 +2,7 @@ # without default 'CMakeLists.txt' file. idf_component_register( - SRCS "main.c" + SRCS "main.c" "application.c" "command_queue.c" INCLUDE_DIRS "../include" REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash ) diff --git a/src/application.c b/src/application.c new file mode 100644 index 0000000..8a311d0 --- /dev/null +++ b/src/application.c @@ -0,0 +1,11 @@ +#include "application.h" + +#include + +bool application_enqueue(application_t *application, const app_command_t *command) { + return application != NULL && command_queue_push(&application->command_queue, command); +} + +bool application_take_next_command(application_t *application, app_command_t *command) { + return application != NULL && command_queue_pop(&application->command_queue, command); +} diff --git a/src/command_queue.c b/src/command_queue.c new file mode 100644 index 0000000..66ed21e --- /dev/null +++ b/src/command_queue.c @@ -0,0 +1,19 @@ +#include "command_queue.h" + +#include + +bool command_queue_push(command_queue_t *queue, const app_command_t *command) { + if (queue == NULL || command == NULL || queue->count == kCommandQueueCapacity) return false; + queue->entries[queue->tail] = *command; + queue->tail = (uint8_t)((queue->tail + 1U) % kCommandQueueCapacity); + ++queue->count; + return true; +} + +bool command_queue_pop(command_queue_t *queue, app_command_t *command) { + if (queue == NULL || command == NULL || queue->count == 0) return false; + *command = queue->entries[queue->head]; + queue->head = (uint8_t)((queue->head + 1U) % kCommandQueueCapacity); + --queue->count; + return true; +} diff --git a/src/main.c b/src/main.c index 8861cff..c187a1b 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,7 @@ #include "esp_check.h" #include "esp_event.h" +#include "esp_heap_caps.h" #include "esp_http_server.h" #include "esp_littlefs.h" #include "esp_log.h" @@ -55,6 +56,7 @@ static bool s_littlefs_mounted; static httpd_handle_t s_server; static esp_timer_handle_t s_state_timer; static portMUX_TYPE s_capacity_lock = portMUX_INITIALIZER_UNLOCKED; +static uint16_t s_rejected_oversized_input; typedef enum { VIEW_PLAYER_1, VIEW_PLAYER_2, VIEW_SPECTATOR } view_role_t; typedef struct { int fd; view_role_t role; } ws_client_t; @@ -260,6 +262,7 @@ static void make_capacity_state(capacity_state_t *state) { static esp_err_t capacity_run_handler(httpd_req_t *request) { if (request->content_len != 0) { + ++s_rejected_oversized_input; return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "capacity run accepts no body"); } capacity_state_t fresh_state; @@ -310,7 +313,10 @@ static esp_err_t websocket_handler(httpd_req_t *request) { } httpd_ws_frame_t frame = {0}; ESP_RETURN_ON_ERROR(httpd_ws_recv_frame(request, &frame, 0), kLogTag, "websocket frame read failed"); - if (frame.len > 64 || frame.type != HTTPD_WS_TYPE_TEXT) return ESP_OK; + if (frame.len > 64 || frame.type != HTTPD_WS_TYPE_TEXT) { + ++s_rejected_oversized_input; + return ESP_OK; + } char message[65] = {0}; frame.payload = (uint8_t *)message; frame.len = sizeof(message) - 1; @@ -462,23 +468,32 @@ static const char *wifi_state_name(const wifi_state_t *state) { static esp_err_t health_handler(httpd_req_t *request) { const wifi_state_t wifi_state = wifi_state_snapshot(); + size_t client_count = kHttpMaxOpenSockets; + int clients[kHttpMaxOpenSockets]; + if (s_server == NULL || httpd_get_client_list(s_server, &client_count, clients) != ESP_OK) client_count = 0; int8_t rssi_dbm = 0; wifi_ap_record_t access_point = {0}; if (wifi_state.connected && esp_wifi_sta_get_ap_info(&access_point) == ESP_OK) { rssi_dbm = access_point.rssi; } - char response[320]; + char response[384]; const int written = snprintf(response, sizeof(response), "{\"uptime_ms\":%" PRIu64 ",\"wifi_state\":\"%s\",\"rssi_dbm\":%d" ",\"free_heap_bytes\":%u,\"min_free_heap_bytes\":%u" + ",\"largest_free_block_bytes\":%u,\"connected_clients\":%u" + ",\"rejected_oversized_input\":%u,\"reset_reason\":%d" ",\"build_version\":\"%s\"}", (uint64_t)(esp_timer_get_time() / 1000), wifi_state_name(&wifi_state), (int)rssi_dbm, (unsigned int)esp_get_free_heap_size(), (unsigned int)esp_get_minimum_free_heap_size(), + (unsigned int)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT), + (unsigned int)client_count, + (unsigned int)s_rejected_oversized_input, + (int)esp_reset_reason(), kBuildVersion); if (written < 0 || (size_t)written >= sizeof(response)) { return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "health response failed"); diff --git a/test/host/Makefile b/test/host/Makefile new file mode 100644 index 0000000..6e18a46 --- /dev/null +++ b/test/host/Makefile @@ -0,0 +1,13 @@ +CC ?= cc +CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include + +all: test_command_queue + +test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/application.c + $(CC) $(CFLAGS) $^ -o $@ + +run: test_command_queue + ./test_command_queue + +clean: + rm -f test_command_queue diff --git a/test/host/test_command_queue b/test/host/test_command_queue new file mode 100755 index 0000000..6b9b172 Binary files /dev/null and b/test/host/test_command_queue differ diff --git a/test/host/test_command_queue.c b/test/host/test_command_queue.c new file mode 100644 index 0000000..232a891 --- /dev/null +++ b/test/host/test_command_queue.c @@ -0,0 +1,24 @@ +#include +#include +#include + +#include "command_queue.h" + +int main(void) { + command_queue_t queue = {0}; + app_command_t input = {.type = COMMAND_SHOT, .session_index = 1, .game_id = 7, .version = 3, .coordinate = {4, 5}}; + app_command_t output = {0}; + assert(!command_queue_pop(&queue, &output)); + for (int index = 0; index < kCommandQueueCapacity; ++index) { + input.coordinate.x = (uint8_t)index; + assert(command_queue_push(&queue, &input)); + } + assert(!command_queue_push(&queue, &input)); + for (int index = 0; index < kCommandQueueCapacity; ++index) { + assert(command_queue_pop(&queue, &output)); + assert(output.coordinate.x == (uint8_t)index); + } + assert(!command_queue_pop(&queue, &output)); + puts("command queue tests passed"); + return 0; +}