feat(game): complete authoritative gameplay mechanics

This commit is contained in:
2026-09-04 12:25:37 +03:00
parent 115a3bd0a1
commit 9339578dc9
13 changed files with 8795 additions and 109 deletions
+2 -2
View File
@@ -98,9 +98,9 @@ Do not silently choose one requirement over another.
### Build and verification
- Parse the project configuration with
`/home/sasa/.platformio/penv/bin/pio project config` after changing
`pio project config` after changing
`platformio.ini`.
- Build with `/home/sasa/.platformio/penv/bin/pio run` once firmware sources
- Build with `pio run` once firmware sources
exist.
- Add host-side deterministic tests for portable game logic before relying only
on device tests.
+65 -6
View File
@@ -555,12 +555,12 @@ server-enforced role-specific visibility.
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`.
provide concrete evidence for all six acceptance criteria and complete
Milestone 006.
## Milestone 007 — Keys, artifacts, compasses, exits, and results
**Status:** `NOT_STARTED`
**Status:** `COMPLETE`
### Objective
@@ -592,9 +592,68 @@ Complete the authoritative gameplay mechanics and terminal match outcomes.
### Evidence
Completion evidence is not yet available because this milestone has not
started. Required evidence includes complete mechanic tests and timestamped
result-window tests.
- `bash scripts/run_host_tests.sh` completed successfully. Its new deterministic
mechanics suite reported `game key inventory, drop, and sabotage: PASS`,
`game atomic meeting key return: PASS (opponent keys swapped to owners)`,
`game owner-only exit opening and outward move: PASS`, `game one-use vision
duration: PASS (10 successful moves, failed move ignored)`, `game compass
bearing and role-specific projections: PASS`, and `game outward result
window: PASS (200 ms draw boundary, 201 ms winner)`. All Milestone 001-006
host regressions and the reproducible web-asset check passed in the same run.
- The key tests exercised pickup of both owner and opponent keys, one-slot
rejection, opponent-key sabotage, deterministic pickup when both keys occupy
one cell, voluntary drop, and rejected empty drop. They asserted after every
transition that each key existed exactly once on the ground or in one
inventory. A crossed-key meeting cleared both inventories and reassigned the
keys to their owners in one state transition.
- Exit tests proved that the wrong key cannot open a player's own exit and that
neither carrying the opponent's key nor standing at the opponent's exit
permits opening or an outward move. Carrying the matching key opened only the
owner's exit; a later outward boundary command was required to enter the
finish window. Timestamped cases proved that the second outward move at
exactly 200 ms produces a draw, while advancing time at 201 ms finalizes the
first player as winner; the symmetric Player 2 winner case also passed.
- Artifact tests consumed an artifact once, selected the authoritative radius
six, retained all ten moves after entry, ignored a failed move, and returned
to radius three after exactly ten later successful moves. Compass tests
produced the direct eight-way bearing to only the activating player's own
exit, preserved it across a rejected command, and cleared it on the next
successful move.
- Projection inspection covered both player roles and the observer. Player
snapshots contained only their own inventory, effect, compass hint, and exit
state; an opponent's carried key appeared only while that opponent was
visible and disappeared when hidden. Observer snapshots contained both
inventories, effects, hints, exit states and timestamps, all remaining
ground objects, and the result. The complete default `128 x 64` observer
snapshot serialized into 688 versioned chunks with a worst-case maximum of
471 bytes, below the 512-byte message bound. Independent AddressSanitizer and
UndefinedBehaviorSanitizer runs of both game suites passed without findings;
leak detection was disabled because the sandbox executes under `ptrace`.
- `PLATFORMIO_SETTING_ENABLE_TELEMETRY=no
/home/sasa/.platformio/penv/bin/pio project config` parsed the locked
`espressif32@7.0.1`, ESP-IDF, `esp32-c6-devkitm-1`, `partitions.csv`, and
`sdkconfig.defaults` configuration, including the isolated
`milestone-007-device-test` environment. `/home/sasa/.platformio/penv/bin/pio
run -e esp32-c6-devkitm-1` completed with `[SUCCESS]`, reporting 47,508 of
327,680 RAM bytes and 1,005,274 of 2,097,152 application bytes; the independent
partition check reported `firmware.bin uses 1005904/2097152 bytes in
partition factory`. The isolated device image also built with `[SUCCESS]`,
using 124,628 RAM bytes and 1,020,168 application bytes, with
`firmware.bin uses 1020800/2097152 bytes in partition factory`.
- `/home/sasa/.platformio/penv/bin/pio run -e esp32-c6-devkitm-1 --target
buildfs` completed with `[SUCCESS]`; the partition check reported
`littlefs.bin uses 2031616/2031616 bytes in partition littlefs`. The test image
upload identified an ESP32-C6FH4 revision 0.2 with 4 MB embedded Flash and
hash-verified each write. The board logged `M007 events: artifact=0, key=0,
hint=8, opened=1, outward=1, result=1, sequence=6`, player and complete
observer projections, `fixed=34776/40960 bytes, runtime=6/50 ms,
heap_delta=0`, and `M007 physical mechanics test: PASS`.
- The normal firmware was restored and hash-verified after the isolated test.
A live `GET /health` returned
`{"status":"serving","healthy":true,"version":"0.7.0","errorCode":0}`.
These deterministic, security-boundary, size, sanitizer, and physical-board
results provide concrete evidence for all six acceptance criteria. Milestone
008 remains `NOT_STARTED`.
## Milestone 008 — Player browser experience
+95 -3
View File
@@ -11,10 +11,15 @@ extern "C" {
#define MAZE32_GAME_PLAYER_COUNT 2U
#define MAZE32_GAME_COMMAND_CAPACITY 16U
#define MAZE32_GAME_KEY_COUNT 2U
#define MAZE32_GAME_NO_KEY UINT8_MAX
#define MAZE32_GAME_VISION_MOVES 10U
#define MAZE32_GAME_DRAW_WINDOW_MS 200U
#define MAZE32_GAME_MAX_ARTIFACTS (2U * MAZE32_MAZE_MAX_ARTIFACT_PAIRS)
#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_CELLS_PER_CHUNK 12U
#define MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK 6U
#define MAZE32_GAME_VIEW_CHUNK_BYTES 512U
#define MAZE32_GAME_RAM_BUDGET_BYTES (40U * 1024U)
#define MAZE32_GAME_RUNTIME_BUDGET_MS 50U
@@ -22,6 +27,8 @@ extern "C" {
typedef enum {
MAZE32_GAME_PHASE_COUNTDOWN = 0,
MAZE32_GAME_PHASE_ACTIVE,
MAZE32_GAME_PHASE_FINISH_PENDING,
MAZE32_GAME_PHASE_FINISHED,
} maze32_game_phase_t;
typedef enum {
@@ -37,6 +44,30 @@ typedef enum {
MAZE32_GAME_DIRECTION_WEST,
} maze32_game_direction_t;
typedef enum {
MAZE32_GAME_COMMAND_MOVE = 0,
MAZE32_GAME_COMMAND_DROP_KEY,
} maze32_game_command_type_t;
typedef enum {
MAZE32_GAME_FINAL_NONE = 0,
MAZE32_GAME_FINAL_PLAYER_ONE,
MAZE32_GAME_FINAL_PLAYER_TWO,
MAZE32_GAME_FINAL_DRAW,
} maze32_game_final_result_t;
typedef enum {
MAZE32_GAME_COMPASS_NONE = 0,
MAZE32_GAME_COMPASS_NORTH,
MAZE32_GAME_COMPASS_NORTH_EAST,
MAZE32_GAME_COMPASS_EAST,
MAZE32_GAME_COMPASS_SOUTH_EAST,
MAZE32_GAME_COMPASS_SOUTH,
MAZE32_GAME_COMPASS_SOUTH_WEST,
MAZE32_GAME_COMPASS_WEST,
MAZE32_GAME_COMPASS_NORTH_WEST,
} maze32_game_compass_direction_t;
typedef enum {
MAZE32_GAME_RESULT_ACCEPTED = 0,
MAZE32_GAME_RESULT_INVALID_ARGUMENT,
@@ -47,6 +78,9 @@ typedef enum {
MAZE32_GAME_RESULT_RATE_LIMITED,
MAZE32_GAME_RESULT_BLOCKED,
MAZE32_GAME_RESULT_BUFFER_TOO_SMALL,
MAZE32_GAME_RESULT_NO_KEY,
MAZE32_GAME_RESULT_PLAYER_EXITED,
MAZE32_GAME_RESULT_MATCH_FINISHED,
} maze32_game_result_t;
typedef struct {
@@ -57,11 +91,31 @@ typedef struct {
maze32_cell_t position;
uint64_t last_move_ms;
uint32_t accepted_moves;
uint8_t carried_key;
uint8_t vision_moves_remaining;
maze32_game_compass_direction_t compass_hint;
uint8_t has_moved;
uint8_t exited;
} maze32_game_player_t;
typedef enum {
MAZE32_GAME_KEY_GROUND = 0,
MAZE32_GAME_KEY_CARRIED,
} maze32_game_key_location_t;
typedef struct {
maze32_game_key_location_t location;
maze32_cell_t ground_position;
uint8_t carrier;
} maze32_game_key_t;
typedef struct {
uint8_t open;
} maze32_game_exit_t;
typedef struct {
uint32_t command_sequence;
maze32_game_command_type_t type;
maze32_game_role_t role;
maze32_game_direction_t direction;
uint64_t submitted_ms;
@@ -73,6 +127,18 @@ typedef struct {
uint32_t state_sequence;
uint32_t meeting_sequence;
uint8_t meeting;
uint8_t moved;
uint8_t dropped_key;
uint8_t picked_key;
uint8_t returned_keys;
uint8_t activated_artifact;
uint8_t opened_exit;
uint8_t outward_exit;
uint8_t finished;
uint8_t key;
uint8_t artifact;
maze32_game_compass_direction_t compass_hint;
maze32_game_final_result_t final_result;
} maze32_game_command_result_t;
typedef struct {
@@ -80,12 +146,19 @@ typedef struct {
maze32_game_config_t config;
maze32_game_phase_t phase;
maze32_game_player_t players[MAZE32_GAME_PLAYER_COUNT];
maze32_game_key_t keys[MAZE32_GAME_KEY_COUNT];
maze32_game_exit_t exits[MAZE32_GAME_PLAYER_COUNT];
uint8_t consumed_artifacts[MAZE32_GAME_MAX_ARTIFACTS];
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_final_result_t final_result;
uint8_t first_exited_player;
uint8_t has_exit_time[MAZE32_GAME_PLAYER_COUNT];
uint64_t exit_times_ms[MAZE32_GAME_PLAYER_COUNT];
} maze32_game_t;
typedef enum {
@@ -113,10 +186,24 @@ typedef struct {
maze32_game_phase_t phase;
uint32_t state_sequence;
uint32_t meeting_sequence;
maze32_game_final_result_t final_result;
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 self_carried_key;
uint8_t self_vision_moves_remaining;
maze32_game_compass_direction_t self_compass_hint;
uint8_t self_exit_open;
uint8_t self_exited;
uint8_t opponent_carried_key;
uint8_t observer_carried_keys[MAZE32_GAME_PLAYER_COUNT];
uint8_t observer_vision_moves_remaining[MAZE32_GAME_PLAYER_COUNT];
maze32_game_compass_direction_t observer_compass_hints[MAZE32_GAME_PLAYER_COUNT];
uint8_t observer_exited[MAZE32_GAME_PLAYER_COUNT];
uint8_t observer_exit_open[MAZE32_GAME_PLAYER_COUNT];
uint8_t observer_has_exit_time[MAZE32_GAME_PLAYER_COUNT];
uint64_t observer_exit_times_ms[MAZE32_GAME_PLAYER_COUNT];
uint8_t object_count;
uint8_t opponent_visible;
maze32_view_cell_t cells[MAZE32_MAZE_MAX_CELLS];
@@ -130,9 +217,14 @@ 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_result_t maze32_game_enqueue_drop_key(maze32_game_t* game, maze32_game_role_t role,
uint64_t submitted_ms,
uint32_t* command_sequence);
maze32_game_command_result_t maze32_game_process_next(maze32_game_t* game);
int maze32_game_advance_time(maze32_game_t* game, uint64_t now_ms);
uint8_t maze32_game_player_view_radius(const maze32_game_t* game, uint8_t player_index);
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);
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,
+408 -64
View File
@@ -15,12 +15,8 @@ static int direction_is_valid(maze32_game_direction_t direction) {
}
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,
};
static const maze32_passage_t PASSAGES[] = {MAZE32_PASSAGE_NORTH, MAZE32_PASSAGE_EAST,
MAZE32_PASSAGE_SOUTH, MAZE32_PASSAGE_WEST};
return PASSAGES[direction];
}
@@ -45,12 +41,24 @@ 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 cells_equal(maze32_cell_t first, maze32_cell_t second) {
return first.x == second.x && first.y == second.y;
}
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;
}
static int direction_leaves_map(const maze32_maze_t* maze, maze32_cell_t cell,
maze32_game_direction_t direction) {
return (direction == MAZE32_GAME_DIRECTION_NORTH && cell.y == 0U) ||
(direction == MAZE32_GAME_DIRECTION_EAST && cell.x + 1U == maze->config.width) ||
(direction == MAZE32_GAME_DIRECTION_SOUTH && cell.y + 1U == maze->config.height) ||
(direction == MAZE32_GAME_DIRECTION_WEST && cell.x == 0U);
}
maze32_game_config_t maze32_game_default_config(void) {
const maze32_game_config_t config = {.movement_cooldown_ms = 150U};
return config;
@@ -70,23 +78,31 @@ maze32_game_result_t maze32_game_init(maze32_game_t* game, const maze32_maze_t*
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;
game->first_exited_player = MAZE32_GAME_NO_OWNER;
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
game->players[player].position = maze->starts[player];
game->players[player].carried_key = MAZE32_GAME_NO_KEY;
game->keys[player].location = MAZE32_GAME_KEY_GROUND;
game->keys[player].ground_position = maze->keys[player];
game->keys[player].carrier = MAZE32_GAME_NO_OWNER;
}
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)) {
if (game != NULL && phase >= MAZE32_GAME_PHASE_COUNTDOWN &&
phase <= MAZE32_GAME_PHASE_FINISHED) {
game->phase = phase;
}
}
maze32_game_result_t maze32_game_enqueue_move(maze32_game_t* game, maze32_game_role_t role,
static maze32_game_result_t enqueue_command(maze32_game_t* game, maze32_game_role_t role,
maze32_game_command_type_t type,
maze32_game_direction_t direction,
uint64_t submitted_ms, uint32_t* command_sequence) {
if (game == NULL || command_sequence == NULL || !direction_is_valid(direction)) {
if (game == NULL || command_sequence == NULL ||
(type == MAZE32_GAME_COMMAND_MOVE && !direction_is_valid(direction))) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
if (!role_is_player(role)) {
@@ -99,6 +115,7 @@ maze32_game_result_t maze32_game_enqueue_move(maze32_game_t* game, maze32_game_r
(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->type = type;
command->role = role;
command->direction = direction;
command->submitted_ms = submitted_ms;
@@ -107,59 +124,286 @@ maze32_game_result_t maze32_game_enqueue_move(maze32_game_t* game, maze32_game_r
return MAZE32_GAME_RESULT_ACCEPTED;
}
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) {
return enqueue_command(game, role, MAZE32_GAME_COMMAND_MOVE, direction, submitted_ms,
command_sequence);
}
maze32_game_result_t maze32_game_enqueue_drop_key(maze32_game_t* game, maze32_game_role_t role,
uint64_t submitted_ms,
uint32_t* command_sequence) {
return enqueue_command(game, role, MAZE32_GAME_COMMAND_DROP_KEY, MAZE32_GAME_DIRECTION_NORTH,
submitted_ms, command_sequence);
}
int maze32_game_advance_time(maze32_game_t* game, uint64_t now_ms) {
if (game == NULL || game->phase != MAZE32_GAME_PHASE_FINISH_PENDING ||
game->first_exited_player >= MAZE32_GAME_PLAYER_COUNT) {
return 0;
}
uint64_t first_exit_ms = game->exit_times_ms[game->first_exited_player];
if (now_ms <= first_exit_ms || now_ms - first_exit_ms <= MAZE32_GAME_DRAW_WINDOW_MS) {
return 0;
}
game->phase = MAZE32_GAME_PHASE_FINISHED;
game->final_result = game->first_exited_player == 0U ? MAZE32_GAME_FINAL_PLAYER_ONE
: MAZE32_GAME_FINAL_PLAYER_TWO;
game->state_sequence++;
return 1;
}
static void accept_player_move(maze32_game_player_t* player, uint64_t submitted_ms) {
player->last_move_ms = submitted_ms;
player->accepted_moves++;
player->has_moved = 1U;
player->compass_hint = MAZE32_GAME_COMPASS_NONE;
if (player->vision_moves_remaining > 0U) {
player->vision_moves_remaining--;
}
}
static uint8_t artifact_at_player(const maze32_game_t* game, maze32_cell_t position) {
for (uint8_t pair = 0U; pair < game->maze->config.artifact_pairs; ++pair) {
for (uint8_t side = 0U; side < 2U; ++side) {
uint8_t artifact = (uint8_t)(pair * 2U + side);
if (!game->consumed_artifacts[artifact] &&
cells_equal(game->maze->artifacts[pair][side], position)) {
return artifact;
}
}
}
return UINT8_MAX;
}
static uint8_t ground_key_at_player(const maze32_game_t* game, maze32_cell_t position) {
for (uint8_t key = 0U; key < MAZE32_GAME_KEY_COUNT; ++key) {
if (game->keys[key].location == MAZE32_GAME_KEY_GROUND &&
cells_equal(game->keys[key].ground_position, position)) {
return key;
}
}
return MAZE32_GAME_NO_KEY;
}
static uint8_t return_keys_to_owners(maze32_game_t* game) {
uint8_t carried[MAZE32_GAME_PLAYER_COUNT];
uint8_t returned = 0U;
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
carried[player] = game->players[player].carried_key;
if (carried[player] != MAZE32_GAME_NO_KEY) {
game->players[player].carried_key = MAZE32_GAME_NO_KEY;
}
}
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
uint8_t key = carried[player];
if (key != MAZE32_GAME_NO_KEY) {
game->players[key].carried_key = key;
game->keys[key].location = MAZE32_GAME_KEY_CARRIED;
game->keys[key].carrier = key;
returned |= (uint8_t)(1U << key);
}
}
return returned;
}
static maze32_game_compass_direction_t compass_direction(maze32_cell_t from, maze32_cell_t to) {
int horizontal = (to.x > from.x) - (to.x < from.x);
int vertical = (to.y > from.y) - (to.y < from.y);
if (vertical < 0 && horizontal == 0) {
return MAZE32_GAME_COMPASS_NORTH;
}
if (vertical < 0 && horizontal > 0) {
return MAZE32_GAME_COMPASS_NORTH_EAST;
}
if (vertical == 0 && horizontal > 0) {
return MAZE32_GAME_COMPASS_EAST;
}
if (vertical > 0 && horizontal > 0) {
return MAZE32_GAME_COMPASS_SOUTH_EAST;
}
if (vertical > 0 && horizontal == 0) {
return MAZE32_GAME_COMPASS_SOUTH;
}
if (vertical > 0 && horizontal < 0) {
return MAZE32_GAME_COMPASS_SOUTH_WEST;
}
if (vertical == 0 && horizontal < 0) {
return MAZE32_GAME_COMPASS_WEST;
}
if (vertical < 0 && horizontal < 0) {
return MAZE32_GAME_COMPASS_NORTH_WEST;
}
return MAZE32_GAME_COMPASS_NONE;
}
static void resolve_cell_entry(maze32_game_t* game, uint8_t player_index,
maze32_game_command_result_t* processed) {
maze32_game_player_t* player = &game->players[player_index];
uint8_t artifact = artifact_at_player(game, player->position);
if (artifact != UINT8_MAX) {
game->consumed_artifacts[artifact] = 1U;
player->vision_moves_remaining = MAZE32_GAME_VISION_MOVES;
processed->activated_artifact = 1U;
processed->artifact = artifact;
}
if (player->carried_key == MAZE32_GAME_NO_KEY) {
uint8_t key = ground_key_at_player(game, player->position);
if (key != MAZE32_GAME_NO_KEY) {
player->carried_key = key;
game->keys[key].location = MAZE32_GAME_KEY_CARRIED;
game->keys[key].carrier = player_index;
processed->picked_key = 1U;
processed->key = key;
}
}
uint8_t opponent_index = (uint8_t)(1U - player_index);
if (cells_equal(player->position, game->players[opponent_index].position)) {
game->meeting_sequence++;
processed->meeting = 1U;
processed->meeting_sequence = game->meeting_sequence;
processed->returned_keys = return_keys_to_owners(game);
}
if (cells_equal(player->position, game->maze->compasses[player_index])) {
player->compass_hint = compass_direction(player->position, game->maze->exits[player_index]);
processed->compass_hint = player->compass_hint;
}
if (!game->exits[player_index].open &&
cells_equal(player->position, game->maze->exits[player_index]) &&
player->carried_key == player_index) {
game->exits[player_index].open = 1U;
processed->opened_exit = 1U;
}
}
static void finish_outward_move(maze32_game_t* game, uint8_t player_index, uint64_t submitted_ms,
maze32_game_command_result_t* processed) {
maze32_game_player_t* player = &game->players[player_index];
accept_player_move(player, submitted_ms);
player->exited = 1U;
game->has_exit_time[player_index] = 1U;
game->exit_times_ms[player_index] = submitted_ms;
processed->moved = 1U;
processed->outward_exit = 1U;
if (game->first_exited_player == MAZE32_GAME_NO_OWNER) {
game->first_exited_player = player_index;
game->phase = MAZE32_GAME_PHASE_FINISH_PENDING;
} else {
game->phase = MAZE32_GAME_PHASE_FINISHED;
game->final_result = MAZE32_GAME_FINAL_DRAW;
processed->finished = 1U;
}
}
static void process_drop_key(maze32_game_t* game, uint8_t player_index,
maze32_game_command_result_t* processed) {
maze32_game_player_t* player = &game->players[player_index];
if (player->carried_key == MAZE32_GAME_NO_KEY) {
processed->result = MAZE32_GAME_RESULT_NO_KEY;
return;
}
uint8_t key = player->carried_key;
player->carried_key = MAZE32_GAME_NO_KEY;
game->keys[key].location = MAZE32_GAME_KEY_GROUND;
game->keys[key].ground_position = player->position;
game->keys[key].carrier = MAZE32_GAME_NO_OWNER;
game->state_sequence++;
processed->result = MAZE32_GAME_RESULT_ACCEPTED;
processed->state_sequence = game->state_sequence;
processed->dropped_key = 1U;
processed->key = key;
}
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};
maze32_game_command_result_t processed = {
.result = MAZE32_GAME_RESULT_INVALID_ARGUMENT,
.key = MAZE32_GAME_NO_KEY,
.artifact = UINT8_MAX,
};
if (game == NULL) {
return processed;
}
processed.state_sequence = game->state_sequence;
processed.meeting_sequence = game->meeting_sequence;
processed.final_result = game->final_result;
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) {
if (maze32_game_advance_time(game, command.submitted_ms)) {
processed.result = MAZE32_GAME_RESULT_MATCH_FINISHED;
processed.state_sequence = game->state_sequence;
processed.final_result = game->final_result;
processed.finished = 1U;
return processed;
}
if (game->phase == MAZE32_GAME_PHASE_FINISHED) {
processed.result = MAZE32_GAME_RESULT_MATCH_FINISHED;
processed.final_result = game->final_result;
return processed;
}
if (game->phase != MAZE32_GAME_PHASE_ACTIVE &&
game->phase != MAZE32_GAME_PHASE_FINISH_PENDING) {
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->exited) {
processed.result = MAZE32_GAME_RESULT_PLAYER_EXITED;
return processed;
}
if (command.type == MAZE32_GAME_COMMAND_DROP_KEY) {
process_drop_key(game, player_index, &processed);
return processed;
}
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;
}
if (direction_leaves_map(game->maze, player->position, command.direction)) {
if (!cells_equal(player->position, game->maze->exits[player_index]) ||
!game->exits[player_index].open) {
processed.result = MAZE32_GAME_RESULT_BLOCKED;
return processed;
}
finish_outward_move(game, player_index, command.submitted_ms, &processed);
} else {
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;
accept_player_move(player, command.submitted_ms);
processed.moved = 1U;
resolve_cell_entry(game, player_index, &processed);
}
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;
}
processed.final_result = game->final_result;
return processed;
}
uint8_t maze32_game_player_view_radius(const maze32_game_t* game, uint8_t player_index) {
if (game == NULL || player_index >= MAZE32_GAME_PLAYER_COUNT) {
return 0U;
}
return game->players[player_index].vision_moves_remaining > 0U ? 6U : 3U;
}
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) {
@@ -177,10 +421,12 @@ static void add_object(const maze32_game_t* game, maze32_view_t* view, maze32_ce
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);
if (game->keys[owner].location == MAZE32_GAME_KEY_GROUND) {
add_object(game, view, game->keys[owner].ground_position, MAZE32_VIEW_OBJECT_KEY, owner,
0U, center, radius, reveal_all);
}
add_object(game, view, game->maze->exits[owner], MAZE32_VIEW_OBJECT_EXIT, owner,
game->exits[owner].open, center, radius, reveal_all);
add_object(game, view, game->maze->compasses[owner], MAZE32_VIEW_OBJECT_COMPASS, owner, 0U,
center, radius, reveal_all);
}
@@ -193,18 +439,20 @@ static void project_objects(const maze32_game_t* game, maze32_view_t* view, maze
}
for (uint8_t pair = 0U; pair < game->maze->config.artifact_pairs; ++pair) {
for (uint8_t side = 0U; side < 2U; ++side) {
uint8_t artifact = (uint8_t)(pair * 2U + side);
if (!game->consumed_artifacts[artifact]) {
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) {
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)) {
(!role_is_player(role) && role != MAZE32_GAME_ROLE_OBSERVER)) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
memset(view, 0, sizeof(*view));
@@ -212,22 +460,44 @@ maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_
view->phase = game->phase;
view->state_sequence = game->state_sequence;
view->meeting_sequence = game->meeting_sequence;
view->final_result = game->final_result;
view->self_carried_key = MAZE32_GAME_NO_KEY;
view->opponent_carried_key = MAZE32_GAME_NO_KEY;
view->opponent_cell_index = UINT16_MAX;
int reveal_all = role == MAZE32_GAME_ROLE_OBSERVER;
uint8_t radius = 0U;
maze32_cell_t center = {0U, 0U};
if (role_is_player(role)) {
uint8_t player_index = (uint8_t)role;
center = game->players[player_index].position;
maze32_game_player_t player = game->players[player_index];
center = player.position;
radius = maze32_game_player_view_radius(game, player_index);
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->self_carried_key = player.carried_key;
view->self_vision_moves_remaining = player.vision_moves_remaining;
view->self_compass_hint = player.compass_hint;
view->self_exit_open = game->exits[player_index].open;
view->self_exited = player.exited;
maze32_game_player_t opponent = game->players[1U - player_index];
if (!opponent.exited && is_visible(center, opponent.position, radius)) {
view->opponent_visible = 1U;
view->opponent_cell_index = cell_index(game->maze, opponent);
view->opponent_cell_index = cell_index(game->maze, opponent.position);
view->opponent_carried_key = opponent.carried_key;
}
} 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 (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
view->observer_player_cell_indices[player] =
cell_index(game->maze, game->players[player].position);
view->observer_carried_keys[player] = game->players[player].carried_key;
view->observer_vision_moves_remaining[player] =
game->players[player].vision_moves_remaining;
view->observer_compass_hints[player] = game->players[player].compass_hint;
view->observer_exited[player] = game->players[player].exited;
view->observer_exit_open[player] = game->exits[player].open;
view->observer_has_exit_time[player] = game->has_exit_time[player];
view->observer_exit_times_ms[player] = game->exit_times_ms[player];
}
}
for (uint16_t y = 0U; y < game->maze->config.height; ++y) {
@@ -284,14 +554,91 @@ static const char* object_name(maze32_view_object_kind_t kind) {
}
static const char* phase_name(maze32_game_phase_t phase) {
return phase == MAZE32_GAME_PHASE_ACTIVE ? "active" : "countdown";
static const char* const NAMES[] = {"countdown", "active", "finish_pending", "finished"};
return NAMES[phase];
}
static const char* final_result_name(maze32_game_final_result_t result) {
static const char* const NAMES[] = {"none", "player1", "player2", "draw"};
return NAMES[result];
}
static const char* compass_name(maze32_game_compass_direction_t direction) {
static const char* const NAMES[] = {"none", "n", "ne", "e", "se", "s", "sw", "w", "nw"};
return NAMES[direction];
}
static int append_nullable_key(char* output, size_t output_size, size_t* offset, const char* name,
uint8_t key) {
if (key == MAZE32_GAME_NO_KEY) {
return append_text(output, output_size, offset, ",\"%s\":null", name);
}
return append_text(output, output_size, offset, ",\"%s\":%u", name, key);
}
static int append_player_status(const maze32_view_t* view, char* output, size_t output_size,
size_t* offset) {
if (!append_text(output, output_size, offset,
",\"self\":%u,\"visionMoves\":%u,\"hint\":\"%s\",\"ownExitOpen\":%u,"
"\"exited\":%u",
view->self_cell_index, view->self_vision_moves_remaining,
compass_name(view->self_compass_hint), view->self_exit_open,
view->self_exited) ||
!append_nullable_key(output, output_size, offset, "inventory", view->self_carried_key)) {
return 0;
}
if (view->opponent_visible &&
(!append_text(output, output_size, offset, ",\"opponent\":%u", view->opponent_cell_index) ||
!append_nullable_key(output, output_size, offset, "opponentKey",
view->opponent_carried_key))) {
return 0;
}
return 1;
}
static int append_observer_status(const maze32_view_t* view, char* output, size_t output_size,
size_t* offset) {
if (!append_text(output, output_size, offset, ",\"players\":[")) {
return 0;
}
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
if (!append_text(output, output_size, offset,
"%s{\"cell\":%u,\"visionMoves\":%u,\"hint\":\"%s\",\"exited\":%u",
player == 0U ? "" : ",", view->observer_player_cell_indices[player],
view->observer_vision_moves_remaining[player],
compass_name(view->observer_compass_hints[player]),
view->observer_exited[player]) ||
!append_nullable_key(output, output_size, offset, "key",
view->observer_carried_keys[player]) ||
!append_text(output, output_size, offset, "}")) {
return 0;
}
}
if (!append_text(output, output_size, offset, "],\"exits\":[%u,%u],\"exitTimes\":[",
view->observer_exit_open[0], view->observer_exit_open[1])) {
return 0;
}
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
if (view->observer_has_exit_time[player]) {
if (!append_text(output, output_size, offset, "%s%llu", player == 0U ? "" : ",",
(unsigned long long)view->observer_exit_times_ms[player])) {
return 0;
}
} else if (!append_text(output, output_size, offset, "%snull", player == 0U ? "" : ",")) {
return 0;
}
}
return append_text(output, output_size, offset, "]");
}
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) {
view->cell_count > MAZE32_MAZE_MAX_CELLS || view->object_count > MAZE32_GAME_MAX_OBJECTS ||
view->role > MAZE32_GAME_ROLE_OBSERVER || view->phase > MAZE32_GAME_PHASE_FINISHED ||
view->final_result > MAZE32_GAME_FINAL_DRAW ||
view->self_compass_hint > MAZE32_GAME_COMPASS_NORTH_WEST) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
size_t total_chunks = maze32_game_view_chunk_count(view);
@@ -301,28 +648,21 @@ maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view,
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",
"\"phase\":\"%s\",\"result\":\"%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)) {
phase_name(view->phase), final_result_name(view->final_result),
(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;
int status_appended = 1;
if (chunk_index == 0U) {
status_appended = view->role == MAZE32_GAME_ROLE_OBSERVER
? append_observer_status(view, output, output_size, &offset)
: append_player_status(view, output, output_size, &offset);
}
} 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\":[")) {
if (!status_appended || !append_text(output, output_size, &offset, ",\"cells\":[")) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
@@ -345,7 +685,6 @@ maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view,
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;
@@ -355,6 +694,9 @@ maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view,
}
for (size_t index = first; index < end; ++index) {
const maze32_view_object_t* object = &view->objects[index];
if (object->kind > MAZE32_VIEW_OBJECT_VISION_ARTIFACT) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
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)) {
@@ -370,10 +712,12 @@ maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view,
}
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) {
static const char* const NAMES[] = {
"accepted", "invalid_argument", "queue_full", "queue_empty",
"read_only", "premature", "rate_limited", "blocked",
"buffer_too_small", "no_key", "player_exited", "match_finished",
};
if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_MATCH_FINISHED) {
return "unknown";
}
return NAMES[result];
+55 -11
View File
@@ -1,8 +1,8 @@
# 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
Milestones 006 and 007 provide the portable movement engine, gameplay object
systems, terminal results, and view projector. The ESP remains the only owner
of mutable game state. Reconnect behavior and browser controls remain out of
scope until their later milestones.
## Command processing
@@ -22,11 +22,12 @@ Observers cannot enqueue commands.
## Projection boundary
Player projections contain only cells whose Chebyshev distance from the
player is at most the active radius:
player is at most the server-owned active radius:
`max(abs(x - player_x), abs(y - player_y)) <= radius`
The projector supports the normal radius three and vision-effect radius six.
The projector selects the normal radius three or vision-effect radius six from
authoritative player state; clients cannot request a radius.
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
@@ -36,10 +37,53 @@ 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`.
and chunk count. Role status appears once in chunk zero instead of being
duplicated across the snapshot. 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.
## Gameplay resolution
Every accepted in-maze move resolves in this order:
1. Decrement an existing vision effect.
2. Consume and activate an artifact on the destination cell.
3. Pick up the lowest-owner-numbered ground key when the inventory is empty.
4. Detect a meeting and atomically return all carried keys to their owners.
5. Activate the moving player's own compass.
6. Open the moving player's own exit when they carry their own key.
The deterministic key priority matters only if voluntary drops place both keys
on one cell. Each key always has exactly one authoritative location: a ground
cell or one carrier. Inventory has one slot, dropping places the key on the
current cell, and carrying an opponent's key is allowed. A meeting clears both
inventory slots before assigning every carried key to its owner, so crossed
keys swap atomically without duplication or loss.
An unconsumed vision artifact is removed on entry and sets the player's effect
counter to ten. Only later successful moves decrement the counter; failed
moves and key drops do not. Entering another unconsumed artifact resets the
counter to ten rather than stacking effects.
Entering the player's associated compass produces one of eight direct compass
bearings toward that player's own exit. It provides neither a coordinate nor a
route. The hint remains through rejected commands and is cleared by the next
successful move unless that move activates the compass again.
## Exits and results
Entering an owner's exit while carrying the matching key opens it but does not
finish the match. The player must issue another movement command pointing out
of the map from that open exit. The first accepted outward move starts a
200-millisecond finish window. A different player exiting at the same timestamp
or up to and including 200 milliseconds later produces a draw. Once time is
strictly more than 200 milliseconds later, advancing the authoritative clock
finalizes the first player as winner. Opponent exits never open or permit an
outward move for the wrong player.
Player snapshots always contain their own inventory, remaining vision moves,
compass hint, and own-exit status. An opponent's carried key is included only
while that opponent is currently visible. Observer snapshots contain both
inventories and effect counters, both exit states and authoritative exit
timestamps, every remaining ground object, and the public match result.
+6
View File
@@ -54,3 +54,9 @@ extends = env:esp32-c6-devkitm-1
build_flags =
${env:esp32-c6-devkitm-1.build_flags}
-DMAZE32_MILESTONE_006_DEVICE_TEST=1
[env:milestone-007-device-test]
extends = env:esp32-c6-devkitm-1
build_flags =
${env:esp32-c6-devkitm-1.build_flags}
-DMAZE32_MILESTONE_007_DEVICE_TEST=1
+10
View File
@@ -68,4 +68,14 @@ cc -std=c11 -Wall -Wextra -Werror -pedantic \
-o "$test_directory/test_game_core"
"$test_directory/test_game_core"
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_mechanics.c" \
-o "$test_directory/test_game_mechanics"
"$test_directory/test_game_mechanics"
python3 "$project_directory/scripts/verify_web_assets.py"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+171 -3
View File
@@ -26,6 +26,167 @@ static httpd_handle_t web_server;
static maze32_network_t network;
static maze32_gateway_t gateway;
#ifdef MAZE32_MILESTONE_007_DEVICE_TEST
static maze32_maze_t mechanics_test_maze;
static maze32_maze_workspace_t mechanics_test_workspace;
static maze32_game_t mechanics_test_game;
static maze32_view_t mechanics_test_view;
static char mechanics_test_payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
static int entry_move_for(const maze32_maze_t* maze, maze32_cell_t target,
maze32_cell_t* starting_position, maze32_game_direction_t* direction) {
if (target.y > 0U && maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_NORTH)) {
*starting_position = (maze32_cell_t){target.x, (uint16_t)(target.y - 1U)};
*direction = MAZE32_GAME_DIRECTION_SOUTH;
return 1;
}
if (target.x + 1U < maze->config.width &&
maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_EAST)) {
*starting_position = (maze32_cell_t){(uint16_t)(target.x + 1U), target.y};
*direction = MAZE32_GAME_DIRECTION_WEST;
return 1;
}
if (target.y + 1U < maze->config.height &&
maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_SOUTH)) {
*starting_position = (maze32_cell_t){target.x, (uint16_t)(target.y + 1U)};
*direction = MAZE32_GAME_DIRECTION_NORTH;
return 1;
}
if (target.x > 0U && maze32_maze_has_passage(maze, target, MAZE32_PASSAGE_WEST)) {
*starting_position = (maze32_cell_t){(uint16_t)(target.x - 1U), target.y};
*direction = MAZE32_GAME_DIRECTION_EAST;
return 1;
}
return 0;
}
static maze32_game_direction_t outward_direction(const maze32_maze_t* maze,
maze32_cell_t exit_cell) {
if (exit_cell.x == 0U) {
return MAZE32_GAME_DIRECTION_WEST;
}
if (exit_cell.x + 1U == maze->config.width) {
return MAZE32_GAME_DIRECTION_EAST;
}
if (exit_cell.y == 0U) {
return MAZE32_GAME_DIRECTION_NORTH;
}
return MAZE32_GAME_DIRECTION_SOUTH;
}
static esp_err_t run_mechanics_device_test(void) {
maze32_maze_config_t maze_config = maze32_maze_default_config();
maze32_maze_report_t maze_report;
if (maze32_maze_generate(&mechanics_test_maze, &mechanics_test_workspace, &maze_config,
UINT32_C(0x5a320007), &maze_report) != MAZE32_MAZE_OK) {
ESP_LOGE(TAG, "M007 physical mechanics test could not generate its maze");
return ESP_FAIL;
}
maze32_game_config_t game_config = maze32_game_default_config();
game_config.movement_cooldown_ms = 0U;
size_t free_before = heap_caps_get_free_size(MALLOC_CAP_8BIT);
int64_t started_us = esp_timer_get_time();
if (maze32_game_init(&mechanics_test_game, &mechanics_test_maze, &game_config) !=
MAZE32_GAME_RESULT_ACCEPTED) {
return ESP_FAIL;
}
maze32_game_set_phase(&mechanics_test_game, MAZE32_GAME_PHASE_ACTIVE);
maze32_cell_t entry_position;
maze32_game_direction_t entry_direction;
uint32_t command_sequence = 0U;
if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.artifacts[0][0], &entry_position,
&entry_direction)) {
return ESP_FAIL;
}
mechanics_test_game.players[0].position = entry_position;
maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction,
100U, &command_sequence);
maze32_game_command_result_t artifact_result = maze32_game_process_next(&mechanics_test_game);
if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.keys[0], &entry_position,
&entry_direction)) {
return ESP_FAIL;
}
mechanics_test_game.players[0].position = entry_position;
maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction,
300U, &command_sequence);
maze32_game_command_result_t key_result = maze32_game_process_next(&mechanics_test_game);
if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.compasses[0], &entry_position,
&entry_direction)) {
return ESP_FAIL;
}
mechanics_test_game.players[0].position = entry_position;
maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction,
500U, &command_sequence);
maze32_game_command_result_t compass_result = maze32_game_process_next(&mechanics_test_game);
if (!entry_move_for(&mechanics_test_maze, mechanics_test_maze.exits[0], &entry_position,
&entry_direction)) {
return ESP_FAIL;
}
mechanics_test_game.players[0].position = entry_position;
maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE, entry_direction,
700U, &command_sequence);
maze32_game_command_result_t exit_result = maze32_game_process_next(&mechanics_test_game);
maze32_game_enqueue_move(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE,
outward_direction(&mechanics_test_maze, mechanics_test_maze.exits[0]),
900U, &command_sequence);
maze32_game_command_result_t outward_result = maze32_game_process_next(&mechanics_test_game);
int finalized = maze32_game_advance_time(&mechanics_test_game, 1101U);
if (artifact_result.result != MAZE32_GAME_RESULT_ACCEPTED ||
!artifact_result.activated_artifact || key_result.result != MAZE32_GAME_RESULT_ACCEPTED ||
!key_result.picked_key || key_result.key != 0U ||
compass_result.compass_hint == MAZE32_GAME_COMPASS_NONE || !exit_result.opened_exit ||
!outward_result.outward_exit || !finalized ||
mechanics_test_game.final_result != MAZE32_GAME_FINAL_PLAYER_ONE ||
maze32_game_project_view(&mechanics_test_game, MAZE32_GAME_ROLE_PLAYER_ONE,
&mechanics_test_view) != MAZE32_GAME_RESULT_ACCEPTED) {
ESP_LOGE(TAG, "M007 physical mechanics transitions failed");
return ESP_FAIL;
}
size_t player_chunks = maze32_game_view_chunk_count(&mechanics_test_view);
size_t payload_bytes = 0U;
if (maze32_game_serialize_view_chunk(&mechanics_test_view, 0U, mechanics_test_payload,
sizeof(mechanics_test_payload),
&payload_bytes) != MAZE32_GAME_RESULT_ACCEPTED ||
maze32_game_project_view(&mechanics_test_game, MAZE32_GAME_ROLE_OBSERVER,
&mechanics_test_view) != MAZE32_GAME_RESULT_ACCEPTED) {
return ESP_FAIL;
}
uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
size_t free_after = heap_caps_get_free_size(MALLOC_CAP_8BIT);
size_t fixed_bytes =
sizeof(mechanics_test_game) + sizeof(mechanics_test_view) + sizeof(mechanics_test_payload);
ESP_LOGI(TAG,
"M007 events: artifact=%u, key=%u, hint=%u, opened=%u, outward=%u, result=%u, "
"sequence=%" PRIu32,
artifact_result.artifact, key_result.key, compass_result.compass_hint,
exit_result.opened_exit, outward_result.outward_exit, mechanics_test_game.final_result,
mechanics_test_game.state_sequence);
ESP_LOGI(TAG,
"M007 views: player_chunks=%u, first_chunk=%u bytes, observer_cells=%u, "
"observer_objects=%u, observer_chunks=%u",
(unsigned int)player_chunks, (unsigned int)payload_bytes,
mechanics_test_view.cell_count, mechanics_test_view.object_count,
(unsigned int)maze32_game_view_chunk_count(&mechanics_test_view));
ESP_LOGI(TAG, "M007 budget: fixed=%u/%u bytes, runtime=%" PRIu64 "/%u ms, heap_delta=%d",
(unsigned int)fixed_bytes, (unsigned int)MAZE32_GAME_RAM_BUDGET_BYTES,
elapsed_us / 1000U, (unsigned int)MAZE32_GAME_RUNTIME_BUDGET_MS,
(int)(free_before - free_after));
if (fixed_bytes > MAZE32_GAME_RAM_BUDGET_BYTES ||
elapsed_us > (uint64_t)MAZE32_GAME_RUNTIME_BUDGET_MS * 1000U || free_before != free_after) {
ESP_LOGE(TAG, "M007 physical mechanics budget failed");
return ESP_FAIL;
}
ESP_LOGI(TAG, "M007 physical mechanics test: PASS");
return ESP_OK;
}
#endif
#ifdef MAZE32_MILESTONE_006_DEVICE_TEST
static maze32_maze_t game_test_maze;
static maze32_maze_workspace_t game_test_workspace;
@@ -82,7 +243,7 @@ static esp_err_t run_game_device_test(void) {
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,
maze32_game_project_view(&device_test_game, MAZE32_GAME_ROLE_PLAYER_ONE,
&device_test_view) != MAZE32_GAME_RESULT_ACCEPTED) {
return ESP_FAIL;
}
@@ -95,8 +256,8 @@ static esp_err_t run_game_device_test(void) {
&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) {
if (maze32_game_project_view(&device_test_game, MAZE32_GAME_ROLE_OBSERVER, &device_test_view) !=
MAZE32_GAME_RESULT_ACCEPTED) {
return ESP_FAIL;
}
uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
@@ -270,6 +431,13 @@ void app_main(void) {
}
#endif
#ifdef MAZE32_MILESTONE_007_DEVICE_TEST
if (run_mechanics_device_test() != ESP_OK) {
maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)ESP_FAIL);
return;
}
#endif
vTaskDelay(pdMS_TO_TICKS(1500));
ESP_LOGI(TAG, "maze32 firmware version %s starting", application->version);
+21 -8
View File
@@ -223,7 +223,7 @@ static void test_visibility_and_serialization(void) {
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) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 49U);
assert(view_has_cell(&view, index_of(6U, 6U)));
@@ -231,7 +231,7 @@ static void test_visibility_and_serialization(void) {
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) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 16U);
assert(!view.opponent_visible && view.opponent_cell_index == UINT16_MAX);
@@ -239,7 +239,7 @@ static void test_visibility_and_serialization(void) {
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) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_TWO, &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)));
@@ -256,18 +256,19 @@ static void test_visibility_and_serialization(void) {
assert(strstr(player_two_payload, "[\"key\",0,0,10]") == NULL);
}
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U, &view) ==
game.players[0].vision_moves_remaining = 1U;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &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) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &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) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 64U);
assert(view.object_count == 10U);
@@ -286,12 +287,24 @@ static void test_default_map_chunk_bounds(void) {
assert(maze32_maze_generate(&maze, &workspace, &maze_config, UINT32_C(0x5a320006), &report) ==
MAZE32_MAZE_OK);
maze32_game_t game = initialized_game(&maze);
game.phase = MAZE32_GAME_PHASE_FINISHED;
game.final_result = MAZE32_GAME_FINAL_DRAW;
game.state_sequence = UINT32_MAX;
game.meeting_sequence = UINT32_MAX;
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
game.players[player].vision_moves_remaining = MAZE32_GAME_VISION_MOVES;
game.players[player].compass_hint = MAZE32_GAME_COMPASS_NORTH_WEST;
game.players[player].exited = 1U;
game.exits[player].open = 1U;
game.has_exit_time[player] = 1U;
game.exit_times_ms[player] = UINT64_MAX - player;
}
maze32_view_t view;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, 0U, &view) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &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);
assert(chunks == 688U);
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
size_t largest_chunk = 0U;
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
+358
View File
@@ -0,0 +1,358 @@
#include "maze32_game.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define TEST_WIDTH 8U
#define TEST_HEIGHT 8U
static uint16_t index_of(uint16_t x, uint16_t y) {
return (uint16_t)(y * TEST_WIDTH + x);
}
static void open_test_grid(maze32_maze_t* maze) {
memset(maze, 0, sizeof(*maze));
maze->config.width = TEST_WIDTH;
maze->config.height = TEST_HEIGHT;
maze->config.artifact_pairs = 1U;
for (uint16_t y = 0U; y < TEST_HEIGHT; ++y) {
for (uint16_t x = 0U; x < TEST_WIDTH; ++x) {
uint8_t passages = 0U;
if (y > 0U) {
passages |= MAZE32_PASSAGE_NORTH;
}
if (x + 1U < TEST_WIDTH) {
passages |= MAZE32_PASSAGE_EAST;
}
if (y + 1U < TEST_HEIGHT) {
passages |= MAZE32_PASSAGE_SOUTH;
}
if (x > 0U) {
passages |= MAZE32_PASSAGE_WEST;
}
maze->passages[index_of(x, y)] = passages;
}
}
maze->starts[0] = (maze32_cell_t){0U, 1U};
maze->starts[1] = (maze32_cell_t){7U, 6U};
maze->keys[0] = (maze32_cell_t){2U, 1U};
maze->keys[1] = (maze32_cell_t){1U, 1U};
maze->exits[0] = (maze32_cell_t){0U, 1U};
maze->exits[1] = (maze32_cell_t){7U, 6U};
maze->compasses[0] = (maze32_cell_t){1U, 1U};
maze->compasses[1] = (maze32_cell_t){6U, 6U};
maze->artifacts[0][0] = (maze32_cell_t){1U, 2U};
maze->artifacts[0][1] = (maze32_cell_t){7U, 7U};
}
static maze32_game_t active_game(maze32_maze_t* maze) {
maze32_game_t game;
maze32_game_config_t config = maze32_game_default_config();
config.movement_cooldown_ms = 0U;
assert(maze32_game_init(&game, maze, &config) == MAZE32_GAME_RESULT_ACCEPTED);
maze32_game_set_phase(&game, MAZE32_GAME_PHASE_ACTIVE);
return game;
}
static maze32_game_command_result_t move_player(maze32_game_t* game, maze32_game_role_t role,
maze32_game_direction_t direction,
uint64_t submitted_ms) {
uint32_t sequence = 0U;
assert(maze32_game_enqueue_move(game, role, direction, submitted_ms, &sequence) ==
MAZE32_GAME_RESULT_ACCEPTED);
maze32_game_command_result_t result = maze32_game_process_next(game);
assert(result.command_sequence == sequence);
return result;
}
static maze32_game_command_result_t drop_key(maze32_game_t* game, maze32_game_role_t role,
uint64_t submitted_ms) {
uint32_t sequence = 0U;
assert(maze32_game_enqueue_drop_key(game, role, submitted_ms, &sequence) ==
MAZE32_GAME_RESULT_ACCEPTED);
maze32_game_command_result_t result = maze32_game_process_next(game);
assert(result.command_sequence == sequence);
return result;
}
static void give_key(maze32_game_t* game, uint8_t carrier, uint8_t key) {
game->players[carrier].carried_key = key;
game->keys[key].location = MAZE32_GAME_KEY_CARRIED;
game->keys[key].carrier = carrier;
}
static void assert_key_conservation(const maze32_game_t* game) {
uint8_t references[MAZE32_GAME_KEY_COUNT] = {0U, 0U};
for (uint8_t key = 0U; key < MAZE32_GAME_KEY_COUNT; ++key) {
if (game->keys[key].location == MAZE32_GAME_KEY_GROUND) {
references[key]++;
assert(game->keys[key].carrier == UINT8_MAX);
} else {
assert(game->keys[key].carrier < MAZE32_GAME_PLAYER_COUNT);
}
}
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
uint8_t key = game->players[player].carried_key;
if (key != MAZE32_GAME_NO_KEY) {
assert(key < MAZE32_GAME_KEY_COUNT);
assert(game->keys[key].location == MAZE32_GAME_KEY_CARRIED);
assert(game->keys[key].carrier == player);
references[key]++;
}
}
assert(references[0] == 1U && references[1] == 1U);
}
static void test_inventory_drop_and_sabotage(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 1U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.picked_key);
assert(result.key == 1U && game.players[0].carried_key == 1U);
assert_key_conservation(&game);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 2U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.picked_key);
assert(game.players[0].carried_key == 1U);
assert(game.keys[0].location == MAZE32_GAME_KEY_GROUND);
assert_key_conservation(&game);
result = drop_key(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.dropped_key && result.key == 1U);
assert(game.players[0].carried_key == MAZE32_GAME_NO_KEY);
assert(game.keys[0].location == MAZE32_GAME_KEY_GROUND &&
game.keys[1].location == MAZE32_GAME_KEY_GROUND);
assert(game.keys[0].ground_position.x == 2U && game.keys[1].ground_position.x == 2U);
assert_key_conservation(&game);
assert(move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 4U).result ==
MAZE32_GAME_RESULT_ACCEPTED);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 5U);
assert(result.picked_key && result.key == 0U);
assert(game.players[0].carried_key == 0U);
assert(game.keys[1].location == MAZE32_GAME_KEY_GROUND);
assert_key_conservation(&game);
uint32_t state_sequence = game.state_sequence;
game.players[0].carried_key = MAZE32_GAME_NO_KEY;
game.keys[0].location = MAZE32_GAME_KEY_GROUND;
game.keys[0].ground_position = game.players[0].position;
game.keys[0].carrier = UINT8_MAX;
result = drop_key(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U);
assert(result.result == MAZE32_GAME_RESULT_NO_KEY && game.state_sequence == state_sequence);
assert_key_conservation(&game);
printf("game key inventory, drop, and sabotage: PASS\n");
}
static void test_atomic_meeting_return(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
game.players[0].position = (maze32_cell_t){2U, 2U};
game.players[1].position = (maze32_cell_t){4U, 2U};
give_key(&game, 0U, 1U);
give_key(&game, 1U, 0U);
assert_key_conservation(&game);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_WEST, 10U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.meeting);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 10U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.meeting);
assert(result.returned_keys == 3U);
assert(game.players[0].carried_key == 0U && game.players[1].carried_key == 1U);
assert(game.keys[0].carrier == 0U && game.keys[1].carrier == 1U);
assert_key_conservation(&game);
printf("game atomic meeting key return: PASS (opponent keys swapped to owners)\n");
}
static void test_owner_exit_restrictions(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
game.players[0].position = (maze32_cell_t){1U, 1U};
give_key(&game, 0U, 1U);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 20U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.opened_exit);
assert(!game.exits[0].open);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 21U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED && game.phase == MAZE32_GAME_PHASE_ACTIVE);
open_test_grid(&maze);
game = active_game(&maze);
game.players[0].position = (maze32_cell_t){6U, 6U};
give_key(&game, 0U, 1U);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 30U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.opened_exit);
assert(!game.exits[1].open);
game.exits[1].open = 1U;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 31U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED && !game.players[0].exited);
open_test_grid(&maze);
game = active_game(&maze);
game.players[0].position = (maze32_cell_t){1U, 1U};
give_key(&game, 0U, 0U);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 40U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.opened_exit);
assert(game.exits[0].open && game.phase == MAZE32_GAME_PHASE_ACTIVE);
assert(game.players[0].carried_key == 0U);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 41U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.outward_exit);
assert(game.phase == MAZE32_GAME_PHASE_FINISH_PENDING &&
game.final_result == MAZE32_GAME_FINAL_NONE);
printf("game owner-only exit opening and outward move: PASS\n");
}
static void test_vision_artifact_duration(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze.starts[0] = (maze32_cell_t){0U, 2U};
maze32_game_t game = active_game(&maze);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 100U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.activated_artifact);
assert(result.artifact == 0U && game.consumed_artifacts[0]);
assert(game.players[0].vision_moves_remaining == MAZE32_GAME_VISION_MOVES);
assert(maze32_game_player_view_radius(&game, 0U) == 6U);
maze.passages[index_of(1U, 2U)] &= (uint8_t)~MAZE32_PASSAGE_NORTH;
uint32_t accepted_moves = game.players[0].accepted_moves;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_NORTH, 101U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED);
assert(game.players[0].vision_moves_remaining == 10U);
assert(game.players[0].accepted_moves == accepted_moves);
for (uint8_t move = 0U; move < MAZE32_GAME_VISION_MOVES; ++move) {
maze32_game_direction_t direction =
move % 2U == 0U ? MAZE32_GAME_DIRECTION_WEST : MAZE32_GAME_DIRECTION_EAST;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, direction, 102U + move);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.activated_artifact);
assert(game.players[0].vision_moves_remaining == MAZE32_GAME_VISION_MOVES - move - 1U);
if (move < MAZE32_GAME_VISION_MOVES - 1U) {
assert(maze32_game_player_view_radius(&game, 0U) == 6U);
}
}
assert(maze32_game_player_view_radius(&game, 0U) == 3U);
printf("game one-use vision duration: PASS (10 successful moves, failed move ignored)\n");
}
static void test_compass_and_role_projections(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze.exits[0] = (maze32_cell_t){7U, 0U};
maze32_game_t game = active_game(&maze);
give_key(&game, 0U, 1U);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 200U);
assert(result.compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
assert(game.players[0].compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
maze32_view_t view;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.self_carried_key == 1U && view.self_compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
size_t written = 0U;
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"hint\":\"ne\"") != NULL);
assert(strstr(payload, "\"inventory\":1") != NULL);
assert(strstr(payload, "\"exitTimes\"") == NULL);
game.keys[0].location = MAZE32_GAME_KEY_CARRIED;
game.keys[0].carrier = 1U;
game.players[1].carried_key = 0U;
game.players[1].position = (maze32_cell_t){2U, 1U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.opponent_visible && view.opponent_carried_key == 0U);
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"opponentKey\":0") != NULL);
game.players[1].position = (maze32_cell_t){7U, 7U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_TWO, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(!view.opponent_visible);
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"hint\":\"ne\"") == NULL);
assert(strstr(payload, "\"opponentKey\"") == NULL);
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.observer_carried_keys[0] == 1U);
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"players\":[") != NULL);
assert(strstr(payload, "\"key\":1") != NULL);
assert(strstr(payload, "\"exitTimes\":[null,null]") != NULL);
maze.passages[index_of(1U, 1U)] &= (uint8_t)~MAZE32_PASSAGE_NORTH;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_NORTH, 201U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED);
assert(game.players[0].compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 202U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
assert(game.players[0].compass_hint == MAZE32_GAME_COMPASS_NONE);
printf("game compass bearing and role-specific projections: PASS\n");
}
static void prepare_open_exits(maze32_game_t* game) {
game->players[0].position = game->maze->exits[0];
game->players[1].position = game->maze->exits[1];
game->exits[0].open = 1U;
game->exits[1].open = 1U;
}
static void test_victory_and_draw_window(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
prepare_open_exits(&game);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 1000U);
assert(result.outward_exit && !result.finished);
assert(game.phase == MAZE32_GAME_PHASE_FINISH_PENDING);
assert(!maze32_game_advance_time(&game, 1200U));
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_EAST, 1200U);
assert(result.outward_exit && result.finished);
assert(game.phase == MAZE32_GAME_PHASE_FINISHED && game.final_result == MAZE32_GAME_FINAL_DRAW);
open_test_grid(&maze);
game = active_game(&maze);
prepare_open_exits(&game);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 2000U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_EAST, 2201U);
assert(result.result == MAZE32_GAME_RESULT_MATCH_FINISHED && result.finished);
assert(!game.players[1].exited && game.final_result == MAZE32_GAME_FINAL_PLAYER_ONE);
open_test_grid(&maze);
game = active_game(&maze);
prepare_open_exits(&game);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_EAST, 3000U);
assert(result.outward_exit && game.final_result == MAZE32_GAME_FINAL_NONE);
assert(!maze32_game_advance_time(&game, 3200U));
assert(maze32_game_advance_time(&game, 3201U));
assert(game.final_result == MAZE32_GAME_FINAL_PLAYER_TWO);
printf("game outward result window: PASS (200 ms draw boundary, 201 ms winner)\n");
}
int main(void) {
test_inventory_drop_and_sabotage();
test_atomic_meeting_return();
test_owner_exit_restrictions();
test_vision_artifact_duration();
test_compass_and_role_projections();
test_victory_and_draw_window();
puts("game mechanics tests: PASS");
return 0;
}
+1 -1
View File
@@ -1 +1 @@
0.6.0
0.7.0