feat(game): complete authoritative gameplay mechanics
This commit is contained in:
@@ -98,9 +98,9 @@ Do not silently choose one requirement over another.
|
|||||||
### Build and verification
|
### Build and verification
|
||||||
|
|
||||||
- Parse the project configuration with
|
- Parse the project configuration with
|
||||||
`/home/sasa/.platformio/penv/bin/pio project config` after changing
|
`pio project config` after changing
|
||||||
`platformio.ini`.
|
`platformio.ini`.
|
||||||
- Build with `/home/sasa/.platformio/penv/bin/pio run` once firmware sources
|
- Build with `pio run` once firmware sources
|
||||||
exist.
|
exist.
|
||||||
- Add host-side deterministic tests for portable game logic before relying only
|
- Add host-side deterministic tests for portable game logic before relying only
|
||||||
on device tests.
|
on device tests.
|
||||||
|
|||||||
@@ -555,12 +555,12 @@ server-enforced role-specific visibility.
|
|||||||
A live `GET /health` returned
|
A live `GET /health` returned
|
||||||
`{"status":"serving","healthy":true,"version":"0.6.0","errorCode":0}`.
|
`{"status":"serving","healthy":true,"version":"0.6.0","errorCode":0}`.
|
||||||
These host, serialization, build, resource, and physical-board results
|
These host, serialization, build, resource, and physical-board results
|
||||||
provide concrete evidence for all six acceptance criteria. Milestone 007
|
provide concrete evidence for all six acceptance criteria and complete
|
||||||
remains `NOT_STARTED`.
|
Milestone 006.
|
||||||
|
|
||||||
## Milestone 007 — Keys, artifacts, compasses, exits, and results
|
## Milestone 007 — Keys, artifacts, compasses, exits, and results
|
||||||
|
|
||||||
**Status:** `NOT_STARTED`
|
**Status:** `COMPLETE`
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
@@ -592,9 +592,68 @@ Complete the authoritative gameplay mechanics and terminal match outcomes.
|
|||||||
|
|
||||||
### Evidence
|
### Evidence
|
||||||
|
|
||||||
Completion evidence is not yet available because this milestone has not
|
- `bash scripts/run_host_tests.sh` completed successfully. Its new deterministic
|
||||||
started. Required evidence includes complete mechanic tests and timestamped
|
mechanics suite reported `game key inventory, drop, and sabotage: PASS`,
|
||||||
result-window tests.
|
`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
|
## Milestone 008 — Player browser experience
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ extern "C" {
|
|||||||
|
|
||||||
#define MAZE32_GAME_PLAYER_COUNT 2U
|
#define MAZE32_GAME_PLAYER_COUNT 2U
|
||||||
#define MAZE32_GAME_COMMAND_CAPACITY 16U
|
#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 \
|
#define MAZE32_GAME_MAX_OBJECTS \
|
||||||
(6U + 2U * MAZE32_MAZE_MAX_LANDMARK_PAIRS + 2U * MAZE32_MAZE_MAX_ARTIFACT_PAIRS)
|
(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_CELLS_PER_CHUNK 12U
|
||||||
#define MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK 10U
|
#define MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK 6U
|
||||||
#define MAZE32_GAME_VIEW_CHUNK_BYTES 512U
|
#define MAZE32_GAME_VIEW_CHUNK_BYTES 512U
|
||||||
#define MAZE32_GAME_RAM_BUDGET_BYTES (40U * 1024U)
|
#define MAZE32_GAME_RAM_BUDGET_BYTES (40U * 1024U)
|
||||||
#define MAZE32_GAME_RUNTIME_BUDGET_MS 50U
|
#define MAZE32_GAME_RUNTIME_BUDGET_MS 50U
|
||||||
@@ -22,6 +27,8 @@ extern "C" {
|
|||||||
typedef enum {
|
typedef enum {
|
||||||
MAZE32_GAME_PHASE_COUNTDOWN = 0,
|
MAZE32_GAME_PHASE_COUNTDOWN = 0,
|
||||||
MAZE32_GAME_PHASE_ACTIVE,
|
MAZE32_GAME_PHASE_ACTIVE,
|
||||||
|
MAZE32_GAME_PHASE_FINISH_PENDING,
|
||||||
|
MAZE32_GAME_PHASE_FINISHED,
|
||||||
} maze32_game_phase_t;
|
} maze32_game_phase_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -37,6 +44,30 @@ typedef enum {
|
|||||||
MAZE32_GAME_DIRECTION_WEST,
|
MAZE32_GAME_DIRECTION_WEST,
|
||||||
} maze32_game_direction_t;
|
} 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 {
|
typedef enum {
|
||||||
MAZE32_GAME_RESULT_ACCEPTED = 0,
|
MAZE32_GAME_RESULT_ACCEPTED = 0,
|
||||||
MAZE32_GAME_RESULT_INVALID_ARGUMENT,
|
MAZE32_GAME_RESULT_INVALID_ARGUMENT,
|
||||||
@@ -47,6 +78,9 @@ typedef enum {
|
|||||||
MAZE32_GAME_RESULT_RATE_LIMITED,
|
MAZE32_GAME_RESULT_RATE_LIMITED,
|
||||||
MAZE32_GAME_RESULT_BLOCKED,
|
MAZE32_GAME_RESULT_BLOCKED,
|
||||||
MAZE32_GAME_RESULT_BUFFER_TOO_SMALL,
|
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;
|
} maze32_game_result_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -57,11 +91,31 @@ typedef struct {
|
|||||||
maze32_cell_t position;
|
maze32_cell_t position;
|
||||||
uint64_t last_move_ms;
|
uint64_t last_move_ms;
|
||||||
uint32_t accepted_moves;
|
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 has_moved;
|
||||||
|
uint8_t exited;
|
||||||
} maze32_game_player_t;
|
} 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 {
|
typedef struct {
|
||||||
uint32_t command_sequence;
|
uint32_t command_sequence;
|
||||||
|
maze32_game_command_type_t type;
|
||||||
maze32_game_role_t role;
|
maze32_game_role_t role;
|
||||||
maze32_game_direction_t direction;
|
maze32_game_direction_t direction;
|
||||||
uint64_t submitted_ms;
|
uint64_t submitted_ms;
|
||||||
@@ -73,6 +127,18 @@ typedef struct {
|
|||||||
uint32_t state_sequence;
|
uint32_t state_sequence;
|
||||||
uint32_t meeting_sequence;
|
uint32_t meeting_sequence;
|
||||||
uint8_t meeting;
|
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;
|
} maze32_game_command_result_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -80,12 +146,19 @@ typedef struct {
|
|||||||
maze32_game_config_t config;
|
maze32_game_config_t config;
|
||||||
maze32_game_phase_t phase;
|
maze32_game_phase_t phase;
|
||||||
maze32_game_player_t players[MAZE32_GAME_PLAYER_COUNT];
|
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];
|
maze32_game_command_t commands[MAZE32_GAME_COMMAND_CAPACITY];
|
||||||
uint8_t command_head;
|
uint8_t command_head;
|
||||||
uint8_t command_count;
|
uint8_t command_count;
|
||||||
uint32_t next_command_sequence;
|
uint32_t next_command_sequence;
|
||||||
uint32_t state_sequence;
|
uint32_t state_sequence;
|
||||||
uint32_t meeting_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;
|
} maze32_game_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -113,10 +186,24 @@ typedef struct {
|
|||||||
maze32_game_phase_t phase;
|
maze32_game_phase_t phase;
|
||||||
uint32_t state_sequence;
|
uint32_t state_sequence;
|
||||||
uint32_t meeting_sequence;
|
uint32_t meeting_sequence;
|
||||||
|
maze32_game_final_result_t final_result;
|
||||||
uint16_t self_cell_index;
|
uint16_t self_cell_index;
|
||||||
uint16_t opponent_cell_index;
|
uint16_t opponent_cell_index;
|
||||||
uint16_t observer_player_cell_indices[MAZE32_GAME_PLAYER_COUNT];
|
uint16_t observer_player_cell_indices[MAZE32_GAME_PLAYER_COUNT];
|
||||||
uint16_t cell_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 object_count;
|
||||||
uint8_t opponent_visible;
|
uint8_t opponent_visible;
|
||||||
maze32_view_cell_t cells[MAZE32_MAZE_MAX_CELLS];
|
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_result_t maze32_game_enqueue_move(maze32_game_t* game, maze32_game_role_t role,
|
||||||
maze32_game_direction_t direction,
|
maze32_game_direction_t direction,
|
||||||
uint64_t submitted_ms, uint32_t* command_sequence);
|
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);
|
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,
|
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);
|
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,
|
maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view, size_t chunk_index,
|
||||||
char* output, size_t output_size,
|
char* output, size_t output_size,
|
||||||
|
|||||||
@@ -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 maze32_passage_t direction_passage(maze32_game_direction_t direction) {
|
||||||
static const maze32_passage_t PASSAGES[] = {
|
static const maze32_passage_t PASSAGES[] = {MAZE32_PASSAGE_NORTH, MAZE32_PASSAGE_EAST,
|
||||||
MAZE32_PASSAGE_NORTH,
|
MAZE32_PASSAGE_SOUTH, MAZE32_PASSAGE_WEST};
|
||||||
MAZE32_PASSAGE_EAST,
|
|
||||||
MAZE32_PASSAGE_SOUTH,
|
|
||||||
MAZE32_PASSAGE_WEST,
|
|
||||||
};
|
|
||||||
return PASSAGES[direction];
|
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;
|
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) {
|
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 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;
|
uint16_t dy = center.y > cell.y ? center.y - cell.y : cell.y - center.y;
|
||||||
return dx <= radius && dy <= radius;
|
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) {
|
maze32_game_config_t maze32_game_default_config(void) {
|
||||||
const maze32_game_config_t config = {.movement_cooldown_ms = 150U};
|
const maze32_game_config_t config = {.movement_cooldown_ms = 150U};
|
||||||
return config;
|
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->maze = maze;
|
||||||
game->config = *config;
|
game->config = *config;
|
||||||
game->phase = MAZE32_GAME_PHASE_COUNTDOWN;
|
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->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;
|
return MAZE32_GAME_RESULT_ACCEPTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
void maze32_game_set_phase(maze32_game_t* game, maze32_game_phase_t phase) {
|
void maze32_game_set_phase(maze32_game_t* game, maze32_game_phase_t phase) {
|
||||||
if (game != NULL &&
|
if (game != NULL && phase >= MAZE32_GAME_PHASE_COUNTDOWN &&
|
||||||
(phase == MAZE32_GAME_PHASE_COUNTDOWN || phase == MAZE32_GAME_PHASE_ACTIVE)) {
|
phase <= MAZE32_GAME_PHASE_FINISHED) {
|
||||||
game->phase = phase;
|
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,
|
maze32_game_direction_t direction,
|
||||||
uint64_t submitted_ms, uint32_t* command_sequence) {
|
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;
|
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
if (!role_is_player(role)) {
|
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);
|
(uint8_t)((game->command_head + game->command_count) % MAZE32_GAME_COMMAND_CAPACITY);
|
||||||
maze32_game_command_t* command = &game->commands[tail];
|
maze32_game_command_t* command = &game->commands[tail];
|
||||||
command->command_sequence = game->next_command_sequence++;
|
command->command_sequence = game->next_command_sequence++;
|
||||||
|
command->type = type;
|
||||||
command->role = role;
|
command->role = role;
|
||||||
command->direction = direction;
|
command->direction = direction;
|
||||||
command->submitted_ms = submitted_ms;
|
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;
|
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 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) {
|
if (game == NULL) {
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
processed.state_sequence = game->state_sequence;
|
processed.state_sequence = game->state_sequence;
|
||||||
processed.meeting_sequence = game->meeting_sequence;
|
processed.meeting_sequence = game->meeting_sequence;
|
||||||
|
processed.final_result = game->final_result;
|
||||||
if (game->command_count == 0U) {
|
if (game->command_count == 0U) {
|
||||||
processed.result = MAZE32_GAME_RESULT_QUEUE_EMPTY;
|
processed.result = MAZE32_GAME_RESULT_QUEUE_EMPTY;
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
maze32_game_command_t command = game->commands[game->command_head];
|
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_head = (uint8_t)((game->command_head + 1U) % MAZE32_GAME_COMMAND_CAPACITY);
|
||||||
game->command_count--;
|
game->command_count--;
|
||||||
processed.command_sequence = command.command_sequence;
|
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;
|
processed.result = MAZE32_GAME_RESULT_PREMATURE;
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t player_index = (uint8_t)command.role;
|
uint8_t player_index = (uint8_t)command.role;
|
||||||
maze32_game_player_t* player = &game->players[player_index];
|
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 &&
|
if (player->has_moved &&
|
||||||
(command.submitted_ms < player->last_move_ms ||
|
(command.submitted_ms < player->last_move_ms ||
|
||||||
command.submitted_ms - player->last_move_ms < game->config.movement_cooldown_ms)) {
|
command.submitted_ms - player->last_move_ms < game->config.movement_cooldown_ms)) {
|
||||||
processed.result = MAZE32_GAME_RESULT_RATE_LIMITED;
|
processed.result = MAZE32_GAME_RESULT_RATE_LIMITED;
|
||||||
return processed;
|
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);
|
maze32_passage_t passage = direction_passage(command.direction);
|
||||||
if (!maze32_maze_has_passage(game->maze, player->position, passage)) {
|
if (!maze32_maze_has_passage(game->maze, player->position, passage)) {
|
||||||
processed.result = MAZE32_GAME_RESULT_BLOCKED;
|
processed.result = MAZE32_GAME_RESULT_BLOCKED;
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
player->position = moved_cell(player->position, command.direction);
|
player->position = moved_cell(player->position, command.direction);
|
||||||
player->last_move_ms = command.submitted_ms;
|
accept_player_move(player, command.submitted_ms);
|
||||||
player->accepted_moves++;
|
processed.moved = 1U;
|
||||||
player->has_moved = 1U;
|
resolve_cell_entry(game, player_index, &processed);
|
||||||
|
}
|
||||||
game->state_sequence++;
|
game->state_sequence++;
|
||||||
processed.result = MAZE32_GAME_RESULT_ACCEPTED;
|
processed.result = MAZE32_GAME_RESULT_ACCEPTED;
|
||||||
processed.state_sequence = game->state_sequence;
|
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.meeting_sequence = game->meeting_sequence;
|
||||||
}
|
processed.final_result = game->final_result;
|
||||||
return processed;
|
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,
|
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_view_object_kind_t kind, uint8_t owner, uint8_t variant,
|
||||||
maze32_cell_t center, uint8_t radius, int reveal_all) {
|
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,
|
static void project_objects(const maze32_game_t* game, maze32_view_t* view, maze32_cell_t center,
|
||||||
uint8_t radius, int reveal_all) {
|
uint8_t radius, int reveal_all) {
|
||||||
for (uint8_t owner = 0U; owner < MAZE32_GAME_PLAYER_COUNT; ++owner) {
|
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,
|
if (game->keys[owner].location == MAZE32_GAME_KEY_GROUND) {
|
||||||
radius, reveal_all);
|
add_object(game, view, game->keys[owner].ground_position, MAZE32_VIEW_OBJECT_KEY, owner,
|
||||||
add_object(game, view, game->maze->exits[owner], MAZE32_VIEW_OBJECT_EXIT, owner, 0U, center,
|
0U, center, radius, reveal_all);
|
||||||
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,
|
add_object(game, view, game->maze->compasses[owner], MAZE32_VIEW_OBJECT_COMPASS, owner, 0U,
|
||||||
center, radius, reveal_all);
|
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 pair = 0U; pair < game->maze->config.artifact_pairs; ++pair) {
|
||||||
for (uint8_t side = 0U; side < 2U; ++side) {
|
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],
|
add_object(game, view, game->maze->artifacts[pair][side],
|
||||||
MAZE32_VIEW_OBJECT_VISION_ARTIFACT, MAZE32_GAME_NO_OWNER, pair, center,
|
MAZE32_VIEW_OBJECT_VISION_ARTIFACT, MAZE32_GAME_NO_OWNER, pair, center,
|
||||||
radius, reveal_all);
|
radius, reveal_all);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_game_role_t role,
|
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 ||
|
if (game == NULL || game->maze == NULL || view == NULL ||
|
||||||
(!role_is_player(role) && role != MAZE32_GAME_ROLE_OBSERVER) ||
|
(!role_is_player(role) && role != MAZE32_GAME_ROLE_OBSERVER)) {
|
||||||
(role_is_player(role) && radius != 3U && radius != 6U)) {
|
|
||||||
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
|
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
memset(view, 0, sizeof(*view));
|
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->phase = game->phase;
|
||||||
view->state_sequence = game->state_sequence;
|
view->state_sequence = game->state_sequence;
|
||||||
view->meeting_sequence = game->meeting_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;
|
view->opponent_cell_index = UINT16_MAX;
|
||||||
int reveal_all = role == MAZE32_GAME_ROLE_OBSERVER;
|
int reveal_all = role == MAZE32_GAME_ROLE_OBSERVER;
|
||||||
|
uint8_t radius = 0U;
|
||||||
maze32_cell_t center = {0U, 0U};
|
maze32_cell_t center = {0U, 0U};
|
||||||
if (role_is_player(role)) {
|
if (role_is_player(role)) {
|
||||||
uint8_t player_index = (uint8_t)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);
|
view->self_cell_index = cell_index(game->maze, center);
|
||||||
maze32_cell_t opponent = game->players[1U - player_index].position;
|
view->self_carried_key = player.carried_key;
|
||||||
if (is_visible(center, opponent, radius)) {
|
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_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 {
|
} else {
|
||||||
view->self_cell_index = UINT16_MAX;
|
view->self_cell_index = UINT16_MAX;
|
||||||
view->observer_player_cell_indices[0] = cell_index(game->maze, game->players[0].position);
|
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
|
||||||
view->observer_player_cell_indices[1] = cell_index(game->maze, game->players[1].position);
|
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) {
|
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) {
|
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,
|
maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view, size_t chunk_index,
|
||||||
char* output, size_t output_size,
|
char* output, size_t output_size,
|
||||||
size_t* written) {
|
size_t* written) {
|
||||||
if (view == NULL || output == NULL || written == NULL || output_size == 0U ||
|
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;
|
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
size_t total_chunks = maze32_game_view_chunk_count(view);
|
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;
|
size_t offset = 0U;
|
||||||
if (!append_text(output, output_size, &offset,
|
if (!append_text(output, output_size, &offset,
|
||||||
"{\"v\":1,\"type\":\"view\",\"seq\":%lu,\"payload\":{\"role\":\"%s\","
|
"{\"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),
|
(unsigned long)view->state_sequence, role_name(view->role),
|
||||||
phase_name(view->phase), (unsigned long)view->meeting_sequence,
|
phase_name(view->phase), final_result_name(view->final_result),
|
||||||
(unsigned int)chunk_index, (unsigned int)total_chunks)) {
|
(unsigned long)view->meeting_sequence, (unsigned int)chunk_index,
|
||||||
|
(unsigned int)total_chunks)) {
|
||||||
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
|
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
|
||||||
}
|
}
|
||||||
if (view->role == MAZE32_GAME_ROLE_OBSERVER) {
|
int status_appended = 1;
|
||||||
if (!append_text(output, output_size, &offset, ",\"players\":[%u,%u]",
|
if (chunk_index == 0U) {
|
||||||
view->observer_player_cell_indices[0],
|
status_appended = view->role == MAZE32_GAME_ROLE_OBSERVER
|
||||||
view->observer_player_cell_indices[1])) {
|
? append_observer_status(view, output, output_size, &offset)
|
||||||
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
|
: append_player_status(view, output, output_size, &offset);
|
||||||
}
|
}
|
||||||
} else if (!append_text(output, output_size, &offset, ",\"self\":%u",
|
if (!status_appended || !append_text(output, output_size, &offset, ",\"cells\":[")) {
|
||||||
(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;
|
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\":[")) {
|
if (!append_text(output, output_size, &offset, "],\"objects\":[")) {
|
||||||
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
|
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunk_index >= cell_chunks) {
|
if (chunk_index >= cell_chunks) {
|
||||||
size_t object_chunk = 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 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) {
|
for (size_t index = first; index < end; ++index) {
|
||||||
const maze32_view_object_t* object = &view->objects[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]",
|
if (!append_text(output, output_size, &offset, "%s[\"%s\",%u,%u,%u]",
|
||||||
index == first ? "" : ",", object_name(object->kind), object->owner,
|
index == first ? "" : ",", object_name(object->kind), object->owner,
|
||||||
object->variant, object->cell_index)) {
|
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) {
|
const char* maze32_game_result_name(maze32_game_result_t result) {
|
||||||
static const char* const NAMES[] = {"accepted", "invalid_argument", "queue_full",
|
static const char* const NAMES[] = {
|
||||||
"queue_empty", "read_only", "premature",
|
"accepted", "invalid_argument", "queue_full", "queue_empty",
|
||||||
"rate_limited", "blocked", "buffer_too_small"};
|
"read_only", "premature", "rate_limited", "blocked",
|
||||||
if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_BUFFER_TOO_SMALL) {
|
"buffer_too_small", "no_key", "player_exited", "match_finished",
|
||||||
|
};
|
||||||
|
if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_MATCH_FINISHED) {
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
return NAMES[result];
|
return NAMES[result];
|
||||||
|
|||||||
+55
-11
@@ -1,8 +1,8 @@
|
|||||||
# Authoritative game core
|
# Authoritative game core
|
||||||
|
|
||||||
Milestone 006 provides the portable movement engine and view projector. The
|
Milestones 006 and 007 provide the portable movement engine, gameplay object
|
||||||
ESP remains the only owner of mutable game state. Object interaction, item
|
systems, terminal results, and view projector. The ESP remains the only owner
|
||||||
effects, exits, results, reconnect behavior, and browser controls remain out of
|
of mutable game state. Reconnect behavior and browser controls remain out of
|
||||||
scope until their later milestones.
|
scope until their later milestones.
|
||||||
|
|
||||||
## Command processing
|
## Command processing
|
||||||
@@ -22,11 +22,12 @@ Observers cannot enqueue commands.
|
|||||||
## Projection boundary
|
## Projection boundary
|
||||||
|
|
||||||
Player projections contain only cells whose Chebyshev distance from the
|
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`
|
`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
|
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
|
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
|
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.
|
matching the WebSocket gateway's message bound.
|
||||||
Every record uses the architecture's versioned envelope and carries the
|
Every record uses the architecture's versioned envelope and carries the
|
||||||
authoritative state sequence, role, match phase, meeting sequence, chunk index,
|
authoritative state sequence, role, match phase, meeting sequence, chunk index,
|
||||||
and chunk count. Cell records contain only the cell index and current passage
|
and chunk count. Role status appears once in chunk zero instead of being
|
||||||
flags. Object records contain kind, owner, variant, and cell index. Player
|
duplicated across the snapshot. Cell records contain only the cell index and
|
||||||
records omit the `opponent` member when that player is not currently visible;
|
current passage flags. Object records contain kind, owner, variant, and cell
|
||||||
observer records instead carry both positions in `players`.
|
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
|
## Gameplay resolution
|
||||||
does not pick up, consume, transfer, or otherwise mutate them.
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -54,3 +54,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_006_DEVICE_TEST=1
|
-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
|
||||||
|
|||||||
@@ -68,4 +68,14 @@ cc -std=c11 -Wall -Wextra -Werror -pedantic \
|
|||||||
-o "$test_directory/test_game_core"
|
-o "$test_directory/test_game_core"
|
||||||
|
|
||||||
"$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"
|
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
@@ -26,6 +26,167 @@ 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_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
|
#ifdef MAZE32_MILESTONE_006_DEVICE_TEST
|
||||||
static maze32_maze_t game_test_maze;
|
static maze32_maze_t game_test_maze;
|
||||||
static maze32_maze_workspace_t game_test_workspace;
|
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);
|
maze32_game_command_result_t command_result = maze32_game_process_next(&device_test_game);
|
||||||
if (command_result.result != MAZE32_GAME_RESULT_ACCEPTED ||
|
if (command_result.result != MAZE32_GAME_RESULT_ACCEPTED ||
|
||||||
command_result.state_sequence != 1U ||
|
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) {
|
&device_test_view) != MAZE32_GAME_RESULT_ACCEPTED) {
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
@@ -95,8 +256,8 @@ static esp_err_t run_game_device_test(void) {
|
|||||||
&payload_bytes) != MAZE32_GAME_RESULT_ACCEPTED) {
|
&payload_bytes) != MAZE32_GAME_RESULT_ACCEPTED) {
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
if (maze32_game_project_view(&device_test_game, MAZE32_GAME_ROLE_OBSERVER, 0U,
|
if (maze32_game_project_view(&device_test_game, MAZE32_GAME_ROLE_OBSERVER, &device_test_view) !=
|
||||||
&device_test_view) != MAZE32_GAME_RESULT_ACCEPTED) {
|
MAZE32_GAME_RESULT_ACCEPTED) {
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
|
uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
|
||||||
@@ -270,6 +431,13 @@ void app_main(void) {
|
|||||||
}
|
}
|
||||||
#endif
|
#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));
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ static void test_visibility_and_serialization(void) {
|
|||||||
game.players[0].position = (maze32_cell_t){3U, 3U};
|
game.players[0].position = (maze32_cell_t){3U, 3U};
|
||||||
game.players[1].position = (maze32_cell_t){4U, 4U};
|
game.players[1].position = (maze32_cell_t){4U, 4U};
|
||||||
maze.passages[index_of(3U, 3U)] = 0U;
|
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);
|
MAZE32_GAME_RESULT_ACCEPTED);
|
||||||
assert(view.cell_count == 49U);
|
assert(view.cell_count == 49U);
|
||||||
assert(view_has_cell(&view, index_of(6U, 6U)));
|
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[0].position = (maze32_cell_t){0U, 0U};
|
||||||
game.players[1].position = (maze32_cell_t){7U, 7U};
|
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);
|
MAZE32_GAME_RESULT_ACCEPTED);
|
||||||
assert(view.cell_count == 16U);
|
assert(view.cell_count == 16U);
|
||||||
assert(!view.opponent_visible && view.opponent_cell_index == UINT16_MAX);
|
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)));
|
assert(!view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(7U, 7U)));
|
||||||
serialize_and_inspect(&view, 1);
|
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);
|
MAZE32_GAME_RESULT_ACCEPTED);
|
||||||
assert(view.cell_count == 16U && view.self_cell_index == index_of(7U, 7U));
|
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.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(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);
|
MAZE32_GAME_RESULT_ACCEPTED);
|
||||||
assert(view.cell_count == 49U);
|
assert(view.cell_count == 49U);
|
||||||
assert(!view_has_cell(&view, index_of(7U, 7U)));
|
assert(!view_has_cell(&view, index_of(7U, 7U)));
|
||||||
|
|
||||||
game.players[0].position = (maze32_cell_t){3U, 3U};
|
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);
|
MAZE32_GAME_RESULT_ACCEPTED);
|
||||||
assert(view.cell_count == 64U);
|
assert(view.cell_count == 64U);
|
||||||
assert(view.opponent_visible);
|
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);
|
MAZE32_GAME_RESULT_ACCEPTED);
|
||||||
assert(view.cell_count == 64U);
|
assert(view.cell_count == 64U);
|
||||||
assert(view.object_count == 10U);
|
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) ==
|
assert(maze32_maze_generate(&maze, &workspace, &maze_config, UINT32_C(0x5a320006), &report) ==
|
||||||
MAZE32_MAZE_OK);
|
MAZE32_MAZE_OK);
|
||||||
maze32_game_t game = initialized_game(&maze);
|
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;
|
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);
|
MAZE32_GAME_RESULT_ACCEPTED);
|
||||||
assert(view.cell_count == 8192U && view.object_count == 30U);
|
assert(view.cell_count == 8192U && view.object_count == 30U);
|
||||||
size_t chunks = maze32_game_view_chunk_count(&view);
|
size_t chunks = maze32_game_view_chunk_count(&view);
|
||||||
assert(chunks == 345U);
|
assert(chunks == 688U);
|
||||||
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
|
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
|
||||||
size_t largest_chunk = 0U;
|
size_t largest_chunk = 0U;
|
||||||
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
|
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
|
||||||
|
|||||||
@@ -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
@@ -1 +1 @@
|
|||||||
0.6.0
|
0.7.0
|
||||||
|
|||||||
Reference in New Issue
Block a user