feat: establish the bounded production architecture

This commit is contained in:
2026-08-28 22:28:24 +03:00
parent 4ff1718307
commit a957d0defc
21 changed files with 235 additions and 5 deletions
+13 -2
View File
@@ -392,7 +392,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record,
## Milestone 006 — Establish the bounded production architecture ## Milestone 006 — Establish the bounded production architecture
**Status:** `READY` **Status:** `DONE`
**Depends on:** Milestone 005 **Depends on:** Milestone 005
### Objective ### 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`. 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 ## Milestone 007 — Implement and exhaustively test the game domain core
**Status:** `BLOCKED` **Status:** `READY`
**Depends on:** Milestone 006 **Depends on:** Milestone 006
### Objective ### Objective
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
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,
};
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
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;
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <stdbool.h>
#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);
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include "app_interfaces.h"
typedef struct { random_source_t random; scheduler_t scheduler; } bot_player_t;
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#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");
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <stdint.h>
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;
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "app_interfaces.h"
#include "game_types.h"
typedef struct { random_source_t random; } fleet_generator_t;
+6
View File
@@ -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;
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#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");
+7
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <stddef.h>
#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);
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <stdint.h>
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;
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include "app_interfaces.h"
typedef struct { transport_t transport; } transport_service_t;
+1 -1
View File
@@ -2,7 +2,7 @@
# without default 'CMakeLists.txt' file. # without default 'CMakeLists.txt' file.
idf_component_register( idf_component_register(
SRCS "main.c" SRCS "main.c" "application.c" "command_queue.c"
INCLUDE_DIRS "../include" INCLUDE_DIRS "../include"
REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
) )
+11
View File
@@ -0,0 +1,11 @@
#include "application.h"
#include <stddef.h>
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);
}
+19
View File
@@ -0,0 +1,19 @@
#include "command_queue.h"
#include <stddef.h>
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;
}
+17 -2
View File
@@ -6,6 +6,7 @@
#include "esp_check.h" #include "esp_check.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_heap_caps.h"
#include "esp_http_server.h" #include "esp_http_server.h"
#include "esp_littlefs.h" #include "esp_littlefs.h"
#include "esp_log.h" #include "esp_log.h"
@@ -55,6 +56,7 @@ static bool s_littlefs_mounted;
static httpd_handle_t s_server; static httpd_handle_t s_server;
static esp_timer_handle_t s_state_timer; static esp_timer_handle_t s_state_timer;
static portMUX_TYPE s_capacity_lock = portMUX_INITIALIZER_UNLOCKED; 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 enum { VIEW_PLAYER_1, VIEW_PLAYER_2, VIEW_SPECTATOR } view_role_t;
typedef struct { int fd; view_role_t role; } ws_client_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) { static esp_err_t capacity_run_handler(httpd_req_t *request) {
if (request->content_len != 0) { if (request->content_len != 0) {
++s_rejected_oversized_input;
return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "capacity run accepts no body"); return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "capacity run accepts no body");
} }
capacity_state_t fresh_state; 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}; httpd_ws_frame_t frame = {0};
ESP_RETURN_ON_ERROR(httpd_ws_recv_frame(request, &frame, 0), kLogTag, "websocket frame read failed"); 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}; char message[65] = {0};
frame.payload = (uint8_t *)message; frame.payload = (uint8_t *)message;
frame.len = sizeof(message) - 1; 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) { static esp_err_t health_handler(httpd_req_t *request) {
const wifi_state_t wifi_state = wifi_state_snapshot(); 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; int8_t rssi_dbm = 0;
wifi_ap_record_t access_point = {0}; wifi_ap_record_t access_point = {0};
if (wifi_state.connected && esp_wifi_sta_get_ap_info(&access_point) == ESP_OK) { if (wifi_state.connected && esp_wifi_sta_get_ap_info(&access_point) == ESP_OK) {
rssi_dbm = access_point.rssi; rssi_dbm = access_point.rssi;
} }
char response[320]; char response[384];
const int written = snprintf(response, const int written = snprintf(response,
sizeof(response), sizeof(response),
"{\"uptime_ms\":%" PRIu64 "{\"uptime_ms\":%" PRIu64
",\"wifi_state\":\"%s\",\"rssi_dbm\":%d" ",\"wifi_state\":\"%s\",\"rssi_dbm\":%d"
",\"free_heap_bytes\":%u,\"min_free_heap_bytes\":%u" ",\"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\"}", ",\"build_version\":\"%s\"}",
(uint64_t)(esp_timer_get_time() / 1000), (uint64_t)(esp_timer_get_time() / 1000),
wifi_state_name(&wifi_state), wifi_state_name(&wifi_state),
(int)rssi_dbm, (int)rssi_dbm,
(unsigned int)esp_get_free_heap_size(), (unsigned int)esp_get_free_heap_size(),
(unsigned int)esp_get_minimum_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); kBuildVersion);
if (written < 0 || (size_t)written >= sizeof(response)) { if (written < 0 || (size_t)written >= sizeof(response)) {
return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "health response failed"); return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "health response failed");
+13
View File
@@ -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
BIN
View File
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#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;
}