feat(game): implement authoritative movement and visibility core
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -1 +1 @@
|
|||||||
0.5.0
|
0.6.0
|
||||||
|
|||||||
Reference in New Issue
Block a user