feat(game): implement authoritative movement and visibility core

This commit is contained in:
2026-09-04 11:29:20 +03:00
parent 5d780def9d
commit 2aaa1650ae
11 changed files with 1085 additions and 7 deletions
+62 -5
View File
@@ -459,11 +459,11 @@ object placement and bounded memory use.
`{"status":"serving","healthy":true,"version":"0.5.0","errorCode":0}`. `{"status":"serving","healthy":true,"version":"0.5.0","errorCode":0}`.
- These deterministic, validator, resource, representative-map, build, and - These deterministic, validator, resource, representative-map, build, and
physical-board results provide concrete evidence for all six acceptance physical-board results provide concrete evidence for all six acceptance
criteria. Milestone 006 remains `NOT_STARTED`. criteria and completed Milestone 005.
## Milestone 006 — Authoritative movement and visibility core ## Milestone 006 — Authoritative movement and visibility core
**Status:** `NOT_STARTED` **Status:** `COMPLETE`
### Objective ### Objective
@@ -497,9 +497,66 @@ server-enforced role-specific visibility.
### Evidence ### Evidence
Completion evidence is not yet available because this milestone has not - On 2026-09-04,
started. Required evidence includes host-test output, serialized payload `PLATFORMIO_SETTING_ENABLE_TELEMETRY=no /home/sasa/.platformio/penv/bin/pio
inspection, and deterministic command-order tests. project config` exited successfully and resolved the production and isolated
`milestone-006-device-test` environments from `espressif32@7.0.1`, ESP-IDF,
the `esp32-c6-devkitm-1` profile, 4 MB Flash, and the root partition and
sdkconfig files.
- `./scripts/run_host_tests.sh` passed every preceding suite and reported
`game movement, cooldown, and FIFO ordering: PASS (commands 1-7, states
0-3)`, `game bounded command queue: PASS (16 entries)`, `game shared-cell
meeting event: PASS (one event)`, and `game visibility and payload security:
PASS (r3=49/16, r6=64/49, observer=64)`. The same suite serialized a complete
default `128 x 64` observer snapshot as 345 chunks and reported a maximum
chunk size of 405 bytes against the 512-byte limit. A separate build with
`-fsanitize=address,undefined` repeated the game-core suite without an
address or undefined-behavior finding; leak detection was disabled because
the sandbox executes under `ptrace`.
- The deterministic movement cases proved that queued commands are processed
in assigned FIFO order, each accepted direction changes exactly one
coordinate by one cell, and Player 1 and Player 2 may act at the same server
timestamp. Countdown, cooldown, out-of-order timestamp, closed-wall,
malformed-direction, observer, and full-queue rejections left player state,
accepted-move counters, cooldown clocks, and the authoritative state
sequence unchanged. Entering the other player's occupied cell left both
players there and emitted one meeting event.
- Projection tests covered both player roles and reused a snapshot after an
opponent changed from visible to hidden. Radius-three squares contained 49
cells at an interior position and 16 at a corner; radius-six squares
contained 64 and 49 respectively on the `8 x 8` fixture. An internal wall
did not remove cells. Player structures and every serialized 512-byte-bounded
JSON chunk omitted hidden cell 63, its hidden key record, and stale opponent
state, while the observer contained all 64 cells, all ten fixture objects,
and both player positions. The serializer also emitted the versioned
envelope, authoritative sequence, phase, meeting sequence, and chunk
metadata.
- `/home/sasa/.platformio/penv/bin/pio run -e esp32-c6-devkitm-1` completed
with `[SUCCESS]` and reported 47,508 of 327,680 RAM bytes and 1,005,274 of
2,097,152 application bytes in use. The production `firmware.bin` measured
1,005,904 bytes against the 2,097,152-byte factory partition.
`/home/sasa/.platformio/penv/bin/pio run -e esp32-c6-devkitm-1 --target
buildfs` completed with `[SUCCESS]`; the independent partition check reported
`littlefs.bin uses 2031616/2031616 bytes in partition littlefs`.
- The isolated device image built with `[SUCCESS]`, using 124,468 of 327,680
RAM bytes and 1,016,242 of 2,097,152 application bytes. Its independent
partition check reported `firmware.bin uses 1016880/2097152 bytes in
partition factory`. Upload with `/home/sasa/.platformio/penv/bin/pio run -e
milestone-006-device-test --target upload --upload-port /dev/ttyACM0`
identified an ESP32-C6FH4 revision 0.2 with 4 MB embedded Flash and
hash-verified every written image.
- The physical board logged an accepted command with command sequence one,
state sequence one, and the new position. It projected 28 player cells into
two bounded chunks and the complete 8,192-cell, 30-object observer state into
345 bounded chunks; the first serialized chunk was 372 bytes. It then logged
`fixed=34612/40960 bytes, runtime=5/50 ms, heap_delta=0` and
`M006 physical game test: PASS`.
- The production image was restored and hash-verified after the isolated test.
A live `GET /health` returned
`{"status":"serving","healthy":true,"version":"0.6.0","errorCode":0}`.
These host, serialization, build, resource, and physical-board results
provide concrete evidence for all six acceptance criteria. Milestone 007
remains `NOT_STARTED`.
## Milestone 007 — Keys, artifacts, compasses, exits, and results ## Milestone 007 — Keys, artifacts, compasses, exits, and results
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "maze32_game.c"
INCLUDE_DIRS "include"
REQUIRES maze32_maze
)
@@ -0,0 +1,146 @@
#ifndef MAZE32_GAME_H
#define MAZE32_GAME_H
#include "maze32_maze.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MAZE32_GAME_PLAYER_COUNT 2U
#define MAZE32_GAME_COMMAND_CAPACITY 16U
#define MAZE32_GAME_MAX_OBJECTS \
(6U + 2U * MAZE32_MAZE_MAX_LANDMARK_PAIRS + 2U * MAZE32_MAZE_MAX_ARTIFACT_PAIRS)
#define MAZE32_GAME_VIEW_CELLS_PER_CHUNK 24U
#define MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK 10U
#define MAZE32_GAME_VIEW_CHUNK_BYTES 512U
#define MAZE32_GAME_RAM_BUDGET_BYTES (40U * 1024U)
#define MAZE32_GAME_RUNTIME_BUDGET_MS 50U
typedef enum {
MAZE32_GAME_PHASE_COUNTDOWN = 0,
MAZE32_GAME_PHASE_ACTIVE,
} maze32_game_phase_t;
typedef enum {
MAZE32_GAME_ROLE_PLAYER_ONE = 0,
MAZE32_GAME_ROLE_PLAYER_TWO,
MAZE32_GAME_ROLE_OBSERVER,
} maze32_game_role_t;
typedef enum {
MAZE32_GAME_DIRECTION_NORTH = 0,
MAZE32_GAME_DIRECTION_EAST,
MAZE32_GAME_DIRECTION_SOUTH,
MAZE32_GAME_DIRECTION_WEST,
} maze32_game_direction_t;
typedef enum {
MAZE32_GAME_RESULT_ACCEPTED = 0,
MAZE32_GAME_RESULT_INVALID_ARGUMENT,
MAZE32_GAME_RESULT_QUEUE_FULL,
MAZE32_GAME_RESULT_QUEUE_EMPTY,
MAZE32_GAME_RESULT_READ_ONLY,
MAZE32_GAME_RESULT_PREMATURE,
MAZE32_GAME_RESULT_RATE_LIMITED,
MAZE32_GAME_RESULT_BLOCKED,
MAZE32_GAME_RESULT_BUFFER_TOO_SMALL,
} maze32_game_result_t;
typedef struct {
uint32_t movement_cooldown_ms;
} maze32_game_config_t;
typedef struct {
maze32_cell_t position;
uint64_t last_move_ms;
uint32_t accepted_moves;
uint8_t has_moved;
} maze32_game_player_t;
typedef struct {
uint32_t command_sequence;
maze32_game_role_t role;
maze32_game_direction_t direction;
uint64_t submitted_ms;
} maze32_game_command_t;
typedef struct {
maze32_game_result_t result;
uint32_t command_sequence;
uint32_t state_sequence;
uint32_t meeting_sequence;
uint8_t meeting;
} maze32_game_command_result_t;
typedef struct {
const maze32_maze_t* maze;
maze32_game_config_t config;
maze32_game_phase_t phase;
maze32_game_player_t players[MAZE32_GAME_PLAYER_COUNT];
maze32_game_command_t commands[MAZE32_GAME_COMMAND_CAPACITY];
uint8_t command_head;
uint8_t command_count;
uint32_t next_command_sequence;
uint32_t state_sequence;
uint32_t meeting_sequence;
} maze32_game_t;
typedef enum {
MAZE32_VIEW_OBJECT_KEY = 0,
MAZE32_VIEW_OBJECT_EXIT,
MAZE32_VIEW_OBJECT_COMPASS,
MAZE32_VIEW_OBJECT_LANDMARK,
MAZE32_VIEW_OBJECT_VISION_ARTIFACT,
} maze32_view_object_kind_t;
typedef struct {
uint16_t cell_index;
uint8_t passages;
} maze32_view_cell_t;
typedef struct {
uint16_t cell_index;
maze32_view_object_kind_t kind;
uint8_t owner;
uint8_t variant;
} maze32_view_object_t;
typedef struct {
maze32_game_role_t role;
maze32_game_phase_t phase;
uint32_t state_sequence;
uint32_t meeting_sequence;
uint16_t self_cell_index;
uint16_t opponent_cell_index;
uint16_t observer_player_cell_indices[MAZE32_GAME_PLAYER_COUNT];
uint16_t cell_count;
uint8_t object_count;
uint8_t opponent_visible;
maze32_view_cell_t cells[MAZE32_MAZE_MAX_CELLS];
maze32_view_object_t objects[MAZE32_GAME_MAX_OBJECTS];
} maze32_view_t;
maze32_game_config_t maze32_game_default_config(void);
maze32_game_result_t maze32_game_init(maze32_game_t* game, const maze32_maze_t* maze,
const maze32_game_config_t* config);
void maze32_game_set_phase(maze32_game_t* game, maze32_game_phase_t phase);
maze32_game_result_t maze32_game_enqueue_move(maze32_game_t* game, maze32_game_role_t role,
maze32_game_direction_t direction,
uint64_t submitted_ms, uint32_t* command_sequence);
maze32_game_command_result_t maze32_game_process_next(maze32_game_t* game);
maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_game_role_t role,
uint8_t radius, maze32_view_t* view);
size_t maze32_game_view_chunk_count(const maze32_view_t* view);
maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view, size_t chunk_index,
char* output, size_t output_size,
size_t* written);
const char* maze32_game_result_name(maze32_game_result_t result);
#ifdef __cplusplus
}
#endif
#endif
+380
View File
@@ -0,0 +1,380 @@
#include "maze32_game.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#define MAZE32_GAME_NO_OWNER UINT8_MAX
static int role_is_player(maze32_game_role_t role) {
return role == MAZE32_GAME_ROLE_PLAYER_ONE || role == MAZE32_GAME_ROLE_PLAYER_TWO;
}
static int direction_is_valid(maze32_game_direction_t direction) {
return direction >= MAZE32_GAME_DIRECTION_NORTH && direction <= MAZE32_GAME_DIRECTION_WEST;
}
static maze32_passage_t direction_passage(maze32_game_direction_t direction) {
static const maze32_passage_t PASSAGES[] = {
MAZE32_PASSAGE_NORTH,
MAZE32_PASSAGE_EAST,
MAZE32_PASSAGE_SOUTH,
MAZE32_PASSAGE_WEST,
};
return PASSAGES[direction];
}
static maze32_cell_t moved_cell(maze32_cell_t cell, maze32_game_direction_t direction) {
if (direction == MAZE32_GAME_DIRECTION_NORTH) {
cell.y--;
} else if (direction == MAZE32_GAME_DIRECTION_EAST) {
cell.x++;
} else if (direction == MAZE32_GAME_DIRECTION_SOUTH) {
cell.y++;
} else {
cell.x--;
}
return cell;
}
static uint16_t cell_index(const maze32_maze_t* maze, maze32_cell_t cell) {
return (uint16_t)(cell.y * maze->config.width + cell.x);
}
static int valid_cell(const maze32_maze_t* maze, maze32_cell_t cell) {
return cell.x < maze->config.width && cell.y < maze->config.height;
}
static int is_visible(maze32_cell_t center, maze32_cell_t cell, uint8_t radius) {
uint16_t dx = center.x > cell.x ? center.x - cell.x : cell.x - center.x;
uint16_t dy = center.y > cell.y ? center.y - cell.y : cell.y - center.y;
return dx <= radius && dy <= radius;
}
maze32_game_config_t maze32_game_default_config(void) {
const maze32_game_config_t config = {.movement_cooldown_ms = 150U};
return config;
}
maze32_game_result_t maze32_game_init(maze32_game_t* game, const maze32_maze_t* maze,
const maze32_game_config_t* config) {
if (game == NULL || maze == NULL || config == NULL || maze->config.width == 0U ||
maze->config.height == 0U || maze->config.width > MAZE32_MAZE_MAX_WIDTH ||
maze->config.height > MAZE32_MAZE_MAX_HEIGHT ||
maze->config.landmark_pairs > MAZE32_MAZE_MAX_LANDMARK_PAIRS ||
maze->config.artifact_pairs > MAZE32_MAZE_MAX_ARTIFACT_PAIRS ||
!valid_cell(maze, maze->starts[0]) || !valid_cell(maze, maze->starts[1])) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
memset(game, 0, sizeof(*game));
game->maze = maze;
game->config = *config;
game->phase = MAZE32_GAME_PHASE_COUNTDOWN;
game->players[0].position = maze->starts[0];
game->players[1].position = maze->starts[1];
game->next_command_sequence = 1U;
return MAZE32_GAME_RESULT_ACCEPTED;
}
void maze32_game_set_phase(maze32_game_t* game, maze32_game_phase_t phase) {
if (game != NULL &&
(phase == MAZE32_GAME_PHASE_COUNTDOWN || phase == MAZE32_GAME_PHASE_ACTIVE)) {
game->phase = phase;
}
}
maze32_game_result_t maze32_game_enqueue_move(maze32_game_t* game, maze32_game_role_t role,
maze32_game_direction_t direction,
uint64_t submitted_ms, uint32_t* command_sequence) {
if (game == NULL || command_sequence == NULL || !direction_is_valid(direction)) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
if (!role_is_player(role)) {
return MAZE32_GAME_RESULT_READ_ONLY;
}
if (game->command_count >= MAZE32_GAME_COMMAND_CAPACITY) {
return MAZE32_GAME_RESULT_QUEUE_FULL;
}
uint8_t tail =
(uint8_t)((game->command_head + game->command_count) % MAZE32_GAME_COMMAND_CAPACITY);
maze32_game_command_t* command = &game->commands[tail];
command->command_sequence = game->next_command_sequence++;
command->role = role;
command->direction = direction;
command->submitted_ms = submitted_ms;
game->command_count++;
*command_sequence = command->command_sequence;
return MAZE32_GAME_RESULT_ACCEPTED;
}
maze32_game_command_result_t maze32_game_process_next(maze32_game_t* game) {
maze32_game_command_result_t processed = {.result = MAZE32_GAME_RESULT_INVALID_ARGUMENT};
if (game == NULL) {
return processed;
}
processed.state_sequence = game->state_sequence;
processed.meeting_sequence = game->meeting_sequence;
if (game->command_count == 0U) {
processed.result = MAZE32_GAME_RESULT_QUEUE_EMPTY;
return processed;
}
maze32_game_command_t command = game->commands[game->command_head];
game->command_head = (uint8_t)((game->command_head + 1U) % MAZE32_GAME_COMMAND_CAPACITY);
game->command_count--;
processed.command_sequence = command.command_sequence;
if (game->phase != MAZE32_GAME_PHASE_ACTIVE) {
processed.result = MAZE32_GAME_RESULT_PREMATURE;
return processed;
}
uint8_t player_index = (uint8_t)command.role;
maze32_game_player_t* player = &game->players[player_index];
if (player->has_moved &&
(command.submitted_ms < player->last_move_ms ||
command.submitted_ms - player->last_move_ms < game->config.movement_cooldown_ms)) {
processed.result = MAZE32_GAME_RESULT_RATE_LIMITED;
return processed;
}
maze32_passage_t passage = direction_passage(command.direction);
if (!maze32_maze_has_passage(game->maze, player->position, passage)) {
processed.result = MAZE32_GAME_RESULT_BLOCKED;
return processed;
}
player->position = moved_cell(player->position, command.direction);
player->last_move_ms = command.submitted_ms;
player->accepted_moves++;
player->has_moved = 1U;
game->state_sequence++;
processed.result = MAZE32_GAME_RESULT_ACCEPTED;
processed.state_sequence = game->state_sequence;
uint8_t opponent_index = (uint8_t)(1U - player_index);
if (player->position.x == game->players[opponent_index].position.x &&
player->position.y == game->players[opponent_index].position.y) {
game->meeting_sequence++;
processed.meeting = 1U;
processed.meeting_sequence = game->meeting_sequence;
}
return processed;
}
static void add_object(const maze32_game_t* game, maze32_view_t* view, maze32_cell_t cell,
maze32_view_object_kind_t kind, uint8_t owner, uint8_t variant,
maze32_cell_t center, uint8_t radius, int reveal_all) {
if (view->object_count >= MAZE32_GAME_MAX_OBJECTS ||
(!reveal_all && !is_visible(center, cell, radius))) {
return;
}
maze32_view_object_t* object = &view->objects[view->object_count++];
object->cell_index = cell_index(game->maze, cell);
object->kind = kind;
object->owner = owner;
object->variant = variant;
}
static void project_objects(const maze32_game_t* game, maze32_view_t* view, maze32_cell_t center,
uint8_t radius, int reveal_all) {
for (uint8_t owner = 0U; owner < MAZE32_GAME_PLAYER_COUNT; ++owner) {
add_object(game, view, game->maze->keys[owner], MAZE32_VIEW_OBJECT_KEY, owner, 0U, center,
radius, reveal_all);
add_object(game, view, game->maze->exits[owner], MAZE32_VIEW_OBJECT_EXIT, owner, 0U, center,
radius, reveal_all);
add_object(game, view, game->maze->compasses[owner], MAZE32_VIEW_OBJECT_COMPASS, owner, 0U,
center, radius, reveal_all);
}
for (uint8_t pair = 0U; pair < game->maze->config.landmark_pairs; ++pair) {
for (uint8_t side = 0U; side < 2U; ++side) {
add_object(game, view, game->maze->landmarks[pair].cells[side],
MAZE32_VIEW_OBJECT_LANDMARK, MAZE32_GAME_NO_OWNER,
game->maze->landmarks[pair].kind, center, radius, reveal_all);
}
}
for (uint8_t pair = 0U; pair < game->maze->config.artifact_pairs; ++pair) {
for (uint8_t side = 0U; side < 2U; ++side) {
add_object(game, view, game->maze->artifacts[pair][side],
MAZE32_VIEW_OBJECT_VISION_ARTIFACT, MAZE32_GAME_NO_OWNER, pair, center,
radius, reveal_all);
}
}
}
maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_game_role_t role,
uint8_t radius, maze32_view_t* view) {
if (game == NULL || game->maze == NULL || view == NULL ||
(!role_is_player(role) && role != MAZE32_GAME_ROLE_OBSERVER) ||
(role_is_player(role) && radius != 3U && radius != 6U)) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
memset(view, 0, sizeof(*view));
view->role = role;
view->phase = game->phase;
view->state_sequence = game->state_sequence;
view->meeting_sequence = game->meeting_sequence;
view->opponent_cell_index = UINT16_MAX;
int reveal_all = role == MAZE32_GAME_ROLE_OBSERVER;
maze32_cell_t center = {0U, 0U};
if (role_is_player(role)) {
uint8_t player_index = (uint8_t)role;
center = game->players[player_index].position;
view->self_cell_index = cell_index(game->maze, center);
maze32_cell_t opponent = game->players[1U - player_index].position;
if (is_visible(center, opponent, radius)) {
view->opponent_visible = 1U;
view->opponent_cell_index = cell_index(game->maze, opponent);
}
} else {
view->self_cell_index = UINT16_MAX;
view->observer_player_cell_indices[0] = cell_index(game->maze, game->players[0].position);
view->observer_player_cell_indices[1] = cell_index(game->maze, game->players[1].position);
}
for (uint16_t y = 0U; y < game->maze->config.height; ++y) {
for (uint16_t x = 0U; x < game->maze->config.width; ++x) {
maze32_cell_t cell = {x, y};
if (reveal_all || is_visible(center, cell, radius)) {
uint16_t index = cell_index(game->maze, cell);
view->cells[view->cell_count].cell_index = index;
view->cells[view->cell_count].passages = game->maze->passages[index];
view->cell_count++;
}
}
}
project_objects(game, view, center, radius, reveal_all);
return MAZE32_GAME_RESULT_ACCEPTED;
}
size_t maze32_game_view_chunk_count(const maze32_view_t* view) {
if (view == NULL) {
return 0U;
}
size_t cell_chunks = (view->cell_count + MAZE32_GAME_VIEW_CELLS_PER_CHUNK - 1U) /
MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
size_t object_chunks = (view->object_count + MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK - 1U) /
MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK;
return cell_chunks + object_chunks;
}
static int append_text(char* output, size_t output_size, size_t* offset, const char* format, ...) {
va_list arguments;
va_start(arguments, format);
int count = vsnprintf(output + *offset, output_size - *offset, format, arguments);
va_end(arguments);
if (count < 0 || (size_t)count >= output_size - *offset) {
return 0;
}
*offset += (size_t)count;
return 1;
}
static const char* role_name(maze32_game_role_t role) {
if (role == MAZE32_GAME_ROLE_PLAYER_ONE) {
return "player1";
}
if (role == MAZE32_GAME_ROLE_PLAYER_TWO) {
return "player2";
}
return "observer";
}
static const char* object_name(maze32_view_object_kind_t kind) {
static const char* const NAMES[] = {"key", "exit", "compass", "landmark", "vision"};
return NAMES[kind];
}
static const char* phase_name(maze32_game_phase_t phase) {
return phase == MAZE32_GAME_PHASE_ACTIVE ? "active" : "countdown";
}
maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view, size_t chunk_index,
char* output, size_t output_size,
size_t* written) {
if (view == NULL || output == NULL || written == NULL || output_size == 0U ||
view->cell_count > MAZE32_MAZE_MAX_CELLS || view->object_count > MAZE32_GAME_MAX_OBJECTS) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
size_t total_chunks = maze32_game_view_chunk_count(view);
if (chunk_index >= total_chunks) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
size_t offset = 0U;
if (!append_text(output, output_size, &offset,
"{\"v\":1,\"type\":\"view\",\"seq\":%lu,\"payload\":{\"role\":\"%s\","
"\"phase\":\"%s\",\"meetingSequence\":%lu,\"chunk\":%u,\"chunks\":%u",
(unsigned long)view->state_sequence, role_name(view->role),
phase_name(view->phase), (unsigned long)view->meeting_sequence,
(unsigned int)chunk_index, (unsigned int)total_chunks)) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
if (view->role == MAZE32_GAME_ROLE_OBSERVER) {
if (!append_text(output, output_size, &offset, ",\"players\":[%u,%u]",
view->observer_player_cell_indices[0],
view->observer_player_cell_indices[1])) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
} else if (!append_text(output, output_size, &offset, ",\"self\":%u",
(unsigned int)view->self_cell_index)) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
if (view->role != MAZE32_GAME_ROLE_OBSERVER && view->opponent_visible &&
!append_text(output, output_size, &offset, ",\"opponent\":%u",
(unsigned int)view->opponent_cell_index)) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
if (!append_text(output, output_size, &offset, ",\"cells\":[")) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
size_t cell_chunks = (view->cell_count + MAZE32_GAME_VIEW_CELLS_PER_CHUNK - 1U) /
MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
if (chunk_index < cell_chunks) {
size_t first = chunk_index * MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
size_t end = first + MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
if (end > view->cell_count) {
end = view->cell_count;
}
for (size_t index = first; index < end; ++index) {
const maze32_view_cell_t* cell = &view->cells[index];
if (!append_text(output, output_size, &offset, "%s[%u,%u]", index == first ? "" : ",",
cell->cell_index, cell->passages)) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
}
}
if (!append_text(output, output_size, &offset, "],\"objects\":[")) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
if (chunk_index >= cell_chunks) {
size_t object_chunk = chunk_index - cell_chunks;
size_t first = object_chunk * MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK;
size_t end = first + MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK;
if (end > view->object_count) {
end = view->object_count;
}
for (size_t index = first; index < end; ++index) {
const maze32_view_object_t* object = &view->objects[index];
if (!append_text(output, output_size, &offset, "%s[\"%s\",%u,%u,%u]",
index == first ? "" : ",", object_name(object->kind), object->owner,
object->variant, object->cell_index)) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
}
}
if (!append_text(output, output_size, &offset, "]}}")) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
*written = offset;
return MAZE32_GAME_RESULT_ACCEPTED;
}
const char* maze32_game_result_name(maze32_game_result_t result) {
static const char* const NAMES[] = {"accepted", "invalid_argument", "queue_full",
"queue_empty", "read_only", "premature",
"rate_limited", "blocked", "buffer_too_small"};
if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_BUFFER_TOO_SMALL) {
return "unknown";
}
return NAMES[result];
}
+45
View File
@@ -0,0 +1,45 @@
# Authoritative game core
Milestone 006 provides the portable movement engine and view projector. The
ESP remains the only owner of mutable game state. Object interaction, item
effects, exits, results, reconnect behavior, and browser controls remain out of
scope until their later milestones.
## Command processing
Move commands enter a fixed 16-entry FIFO. The server assigns a monotonically
increasing command sequence when an entry is accepted into the queue, then
processes entries one at a time. An accepted move crosses exactly one open
north, east, south, or west passage. Countdown commands, closed-wall moves,
out-of-order timestamps, and commands inside the configured per-player
cooldown leave authoritative game state and the state sequence unchanged.
Each player has an independent cooldown clock. Player positions never block
one another. When an accepted move ends on the other player's cell, that
command emits exactly one meeting event and advances the meeting sequence.
Observers cannot enqueue commands.
## Projection boundary
Player projections contain only cells whose Chebyshev distance from the
player is at most the active radius:
`max(abs(x - player_x), abs(y - player_y)) <= radius`
The projector supports the normal radius three and vision-effect radius six.
Map boundaries clip the square; walls do not occlude it. Each projection is
rebuilt from zero, so an opponent or object that leaves the current square is
omitted instead of surviving as stale state. Observer projections contain all
maze cells, static objects, and both current player positions.
The serialized view is split into bounded JSON records of at most 512 bytes,
matching the WebSocket gateway's message bound.
Every record uses the architecture's versioned envelope and carries the
authoritative state sequence, role, match phase, meeting sequence, chunk index,
and chunk count. Cell records contain only the cell index and current passage
flags. Object records contain kind, owner, variant, and cell index. Player
records omit the `opponent` member when that player is not currently visible;
observer records instead carry both positions in `players`.
Static maze objects are projected for visibility testing, but Milestone 006
does not pick up, consume, transfer, or otherwise mutate them.
+6
View File
@@ -48,3 +48,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_005_DEVICE_TEST=1 -DMAZE32_MILESTONE_005_DEVICE_TEST=1
[env:milestone-006-device-test]
extends = env:esp32-c6-devkitm-1
build_flags =
${env:esp32-c6-devkitm-1.build_flags}
-DMAZE32_MILESTONE_006_DEVICE_TEST=1
+10
View File
@@ -58,4 +58,14 @@ cc -std=c11 -Wall -Wextra -Werror -pedantic \
-o "$test_directory/test_maze_generation" -o "$test_directory/test_maze_generation"
"$test_directory/test_maze_generation" "$test_directory/test_maze_generation"
cc -std=c11 -Wall -Wextra -Werror -pedantic \
-I"$project_directory/components/maze32_maze/include" \
-I"$project_directory/components/maze32_game/include" \
"$project_directory/components/maze32_maze/maze32_maze.c" \
"$project_directory/components/maze32_game/maze32_game.c" \
"$project_directory/test/host/test_game_core.c" \
-o "$test_directory/test_game_core"
"$test_directory/test_game_core"
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 esp_timer heap maze32_config maze32_maze maze32_network maze32_platform maze32_web REQUIRES esp_event esp_netif esp_timer heap maze32_config maze32_game maze32_maze maze32_network maze32_platform maze32_web
) )
+111
View File
@@ -14,6 +14,7 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "maze32_config_store.h" #include "maze32_config_store.h"
#include "maze32_game.h"
#include "maze32_health.h" #include "maze32_health.h"
#include "maze32_maze.h" #include "maze32_maze.h"
#include "maze32_network.h" #include "maze32_network.h"
@@ -25,6 +26,109 @@ 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_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, 3U,
&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, 0U,
&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 #ifdef MAZE32_MILESTONE_005_DEVICE_TEST
static maze32_maze_t device_test_maze; static maze32_maze_t device_test_maze;
static maze32_maze_workspace_t device_test_workspace; static maze32_maze_workspace_t device_test_workspace;
@@ -159,6 +263,13 @@ void app_main(void) {
} }
#endif #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
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);
+318
View File
@@ -0,0 +1,318 @@
#include "maze32_game.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static uint16_t index_of(uint16_t x, uint16_t y) {
return (uint16_t)(y * 8U + x);
}
static void open_test_grid(maze32_maze_t* maze) {
memset(maze, 0, sizeof(*maze));
maze->config.width = 8U;
maze->config.height = 8U;
maze->config.landmark_pairs = 1U;
maze->config.artifact_pairs = 1U;
for (uint16_t y = 0U; y < 8U; ++y) {
for (uint16_t x = 0U; x < 8U; ++x) {
uint8_t passages = 0U;
if (y > 0U) {
passages |= MAZE32_PASSAGE_NORTH;
}
if (x < 7U) {
passages |= MAZE32_PASSAGE_EAST;
}
if (y < 7U) {
passages |= MAZE32_PASSAGE_SOUTH;
}
if (x > 0U) {
passages |= MAZE32_PASSAGE_WEST;
}
maze->passages[index_of(x, y)] = passages;
}
}
maze->starts[0] = (maze32_cell_t){1U, 1U};
maze->starts[1] = (maze32_cell_t){6U, 6U};
maze->keys[0] = (maze32_cell_t){2U, 1U};
maze->keys[1] = (maze32_cell_t){7U, 7U};
maze->exits[0] = (maze32_cell_t){0U, 0U};
maze->exits[1] = (maze32_cell_t){7U, 6U};
maze->compasses[0] = (maze32_cell_t){1U, 0U};
maze->compasses[1] = (maze32_cell_t){6U, 7U};
maze->landmarks[0].cells[0] = (maze32_cell_t){0U, 1U};
maze->landmarks[0].cells[1] = (maze32_cell_t){7U, 6U};
maze->landmarks[0].kind = 4U;
maze->artifacts[0][0] = (maze32_cell_t){2U, 2U};
maze->artifacts[0][1] = (maze32_cell_t){6U, 6U};
}
static maze32_game_t initialized_game(maze32_maze_t* maze) {
maze32_game_t game;
maze32_game_config_t config = maze32_game_default_config();
assert(config.movement_cooldown_ms == 150U);
assert(maze32_game_init(&game, maze, &config) == MAZE32_GAME_RESULT_ACCEPTED);
return game;
}
static void assert_position(const maze32_game_t* game, uint8_t player, uint16_t x, uint16_t y) {
assert(game->players[player].position.x == x);
assert(game->players[player].position.y == y);
}
static void test_movement_cooldown_and_ordering(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = initialized_game(&maze);
uint32_t sequence = 0U;
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
100U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
assert(sequence == 1U);
maze32_game_command_result_t result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_PREMATURE);
assert(result.command_sequence == 1U);
assert(game.state_sequence == 0U);
assert_position(&game, 0U, 1U, 1U);
maze32_game_set_phase(&game, MAZE32_GAME_PHASE_ACTIVE);
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
100U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
assert(sequence == 2U);
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_WEST,
100U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
assert(sequence == 3U);
result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
assert(result.command_sequence == 2U && result.state_sequence == 1U);
assert_position(&game, 0U, 2U, 1U);
assert_position(&game, 1U, 6U, 6U);
result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
assert(result.command_sequence == 3U && result.state_sequence == 2U);
assert_position(&game, 1U, 5U, 6U);
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_SOUTH,
200U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_RATE_LIMITED);
assert(game.state_sequence == 2U && game.players[0].last_move_ms == 100U);
assert_position(&game, 0U, 2U, 1U);
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_SOUTH,
250U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
assert_position(&game, 0U, 2U, 2U);
assert(game.players[0].accepted_moves == 2U);
uint16_t current = index_of(2U, 2U);
maze.passages[current] &= (uint8_t)~MAZE32_PASSAGE_EAST;
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
400U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED);
assert(game.state_sequence == 3U && game.players[0].last_move_ms == 250U);
assert_position(&game, 0U, 2U, 2U);
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST,
200U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_RATE_LIMITED);
assert_position(&game, 0U, 2U, 2U);
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_OBSERVER, MAZE32_GAME_DIRECTION_EAST,
500U, &sequence) == MAZE32_GAME_RESULT_READ_ONLY);
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, (maze32_game_direction_t)99,
500U, &sequence) == MAZE32_GAME_RESULT_INVALID_ARGUMENT);
assert(game.state_sequence == 3U);
printf("game movement, cooldown, and FIFO ordering: PASS (commands 1-7, states 0-3)\n");
}
static void test_bounded_queue(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = initialized_game(&maze);
uint32_t sequence = 0U;
for (uint32_t index = 0U; index < MAZE32_GAME_COMMAND_CAPACITY; ++index) {
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE,
MAZE32_GAME_DIRECTION_EAST, index,
&sequence) == MAZE32_GAME_RESULT_ACCEPTED);
assert(sequence == index + 1U);
}
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
20U, &sequence) == MAZE32_GAME_RESULT_QUEUE_FULL);
assert(game.next_command_sequence == MAZE32_GAME_COMMAND_CAPACITY + 1U);
assert(game.state_sequence == 0U);
printf("game bounded command queue: PASS (%u entries)\n", MAZE32_GAME_COMMAND_CAPACITY);
}
static void test_shared_cell_meeting(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze.starts[1] = (maze32_cell_t){2U, 1U};
maze32_game_t game = initialized_game(&maze);
maze32_game_set_phase(&game, MAZE32_GAME_PHASE_ACTIVE);
uint32_t sequence = 0U;
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
10U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
maze32_game_command_result_t result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
assert(result.meeting == 1U && result.meeting_sequence == 1U);
assert_position(&game, 0U, 2U, 1U);
assert_position(&game, 1U, 2U, 1U);
result = maze32_game_process_next(&game);
assert(result.result == MAZE32_GAME_RESULT_QUEUE_EMPTY);
assert(result.meeting == 0U && result.meeting_sequence == 1U);
printf("game shared-cell meeting event: PASS (one event)\n");
}
static int view_has_cell(const maze32_view_t* view, uint16_t cell_index) {
for (uint16_t index = 0U; index < view->cell_count; ++index) {
if (view->cells[index].cell_index == cell_index) {
return 1;
}
}
return 0;
}
static int view_has_object(const maze32_view_t* view, maze32_view_object_kind_t kind,
uint16_t cell_index) {
for (uint8_t index = 0U; index < view->object_count; ++index) {
if (view->objects[index].kind == kind && view->objects[index].cell_index == cell_index) {
return 1;
}
}
return 0;
}
static void serialize_and_inspect(const maze32_view_t* view, int expect_hidden) {
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
size_t chunks = maze32_game_view_chunk_count(view);
int saw_last_cell = 0;
int saw_hidden_key = 0;
int saw_players = 0;
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
size_t written = 0U;
assert(maze32_game_serialize_view_chunk(view, chunk, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(written < sizeof(payload));
assert(payload[written] == '\0');
assert(strstr(payload, "{\"v\":1,\"type\":\"view\",\"seq\":") == payload);
assert(strstr(payload, "\"phase\":\"countdown\"") != NULL);
saw_last_cell |= strstr(payload, "[63,") != NULL;
saw_hidden_key |= strstr(payload, "[\"key\",1,0,63]") != NULL;
saw_players |= strstr(payload, "\"players\":[") != NULL;
if (expect_hidden) {
assert(strstr(payload, "\"opponent\":63") == NULL);
}
}
if (expect_hidden) {
assert(!saw_last_cell && !saw_hidden_key && !saw_players);
} else {
assert(saw_last_cell && saw_hidden_key && saw_players);
}
}
static void test_visibility_and_serialization(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = initialized_game(&maze);
maze32_view_t view;
game.players[0].position = (maze32_cell_t){3U, 3U};
game.players[1].position = (maze32_cell_t){4U, 4U};
maze.passages[index_of(3U, 3U)] = 0U;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 49U);
assert(view_has_cell(&view, index_of(6U, 6U)));
assert(view.opponent_visible && view.opponent_cell_index == index_of(4U, 4U));
game.players[0].position = (maze32_cell_t){0U, 0U};
game.players[1].position = (maze32_cell_t){7U, 7U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 16U);
assert(!view.opponent_visible && view.opponent_cell_index == UINT16_MAX);
assert(view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(2U, 1U)));
assert(!view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(7U, 7U)));
serialize_and_inspect(&view, 1);
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_TWO, 3U, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 16U && view.self_cell_index == index_of(7U, 7U));
assert(!view.opponent_visible && !view_has_cell(&view, index_of(0U, 0U)));
assert(view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(7U, 7U)));
assert(!view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(2U, 1U)));
char player_two_payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
for (size_t chunk = 0U; chunk < maze32_game_view_chunk_count(&view); ++chunk) {
size_t written = 0U;
assert(maze32_game_serialize_view_chunk(&view, chunk, player_two_payload,
sizeof(player_two_payload),
&written) == MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(player_two_payload, "\"role\":\"player2\"") != NULL);
assert(strstr(player_two_payload, "\"opponent\":0") == NULL);
assert(strstr(player_two_payload, "[\"key\",0,0,10]") == NULL);
}
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 49U);
assert(!view_has_cell(&view, index_of(7U, 7U)));
game.players[0].position = (maze32_cell_t){3U, 3U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 64U);
assert(view.opponent_visible);
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, 0U, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 64U);
assert(view.object_count == 10U);
assert(view.observer_player_cell_indices[0] == index_of(3U, 3U));
assert(view.observer_player_cell_indices[1] == index_of(7U, 7U));
assert(view.phase == MAZE32_GAME_PHASE_COUNTDOWN);
serialize_and_inspect(&view, 0);
printf("game visibility and payload security: PASS (r3=49/16, r6=64/49, observer=64)\n");
}
static void test_default_map_chunk_bounds(void) {
maze32_maze_t maze;
maze32_maze_workspace_t workspace;
maze32_maze_config_t maze_config = maze32_maze_default_config();
maze32_maze_report_t report;
assert(maze32_maze_generate(&maze, &workspace, &maze_config, UINT32_C(0x5a320006), &report) ==
MAZE32_MAZE_OK);
maze32_game_t game = initialized_game(&maze);
maze32_view_t view;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, 0U, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 8192U && view.object_count == 30U);
size_t chunks = maze32_game_view_chunk_count(&view);
assert(chunks == 345U);
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
size_t largest_chunk = 0U;
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
size_t written = 0U;
assert(maze32_game_serialize_view_chunk(&view, chunk, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
if (written > largest_chunk) {
largest_chunk = written;
}
}
assert(largest_chunk < MAZE32_GAME_VIEW_CHUNK_BYTES);
printf("game default observer serialization: PASS (%u chunks, maximum %u bytes)\n",
(unsigned int)chunks, (unsigned int)largest_chunk);
}
int main(void) {
test_movement_cooldown_and_ordering();
test_bounded_queue();
test_shared_cell_meeting();
test_visibility_and_serialization();
test_default_map_chunk_bounds();
puts("game core tests: PASS");
return 0;
}
+1 -1
View File
@@ -1 +1 @@
0.5.0 0.6.0