feat(game): implement authoritative movement and visibility core
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
#include "maze32_game.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static uint16_t index_of(uint16_t x, uint16_t y) {
|
||||
return (uint16_t)(y * 8U + x);
|
||||
}
|
||||
|
||||
static void open_test_grid(maze32_maze_t* maze) {
|
||||
memset(maze, 0, sizeof(*maze));
|
||||
maze->config.width = 8U;
|
||||
maze->config.height = 8U;
|
||||
maze->config.landmark_pairs = 1U;
|
||||
maze->config.artifact_pairs = 1U;
|
||||
for (uint16_t y = 0U; y < 8U; ++y) {
|
||||
for (uint16_t x = 0U; x < 8U; ++x) {
|
||||
uint8_t passages = 0U;
|
||||
if (y > 0U) {
|
||||
passages |= MAZE32_PASSAGE_NORTH;
|
||||
}
|
||||
if (x < 7U) {
|
||||
passages |= MAZE32_PASSAGE_EAST;
|
||||
}
|
||||
if (y < 7U) {
|
||||
passages |= MAZE32_PASSAGE_SOUTH;
|
||||
}
|
||||
if (x > 0U) {
|
||||
passages |= MAZE32_PASSAGE_WEST;
|
||||
}
|
||||
maze->passages[index_of(x, y)] = passages;
|
||||
}
|
||||
}
|
||||
maze->starts[0] = (maze32_cell_t){1U, 1U};
|
||||
maze->starts[1] = (maze32_cell_t){6U, 6U};
|
||||
maze->keys[0] = (maze32_cell_t){2U, 1U};
|
||||
maze->keys[1] = (maze32_cell_t){7U, 7U};
|
||||
maze->exits[0] = (maze32_cell_t){0U, 0U};
|
||||
maze->exits[1] = (maze32_cell_t){7U, 6U};
|
||||
maze->compasses[0] = (maze32_cell_t){1U, 0U};
|
||||
maze->compasses[1] = (maze32_cell_t){6U, 7U};
|
||||
maze->landmarks[0].cells[0] = (maze32_cell_t){0U, 1U};
|
||||
maze->landmarks[0].cells[1] = (maze32_cell_t){7U, 6U};
|
||||
maze->landmarks[0].kind = 4U;
|
||||
maze->artifacts[0][0] = (maze32_cell_t){2U, 2U};
|
||||
maze->artifacts[0][1] = (maze32_cell_t){6U, 6U};
|
||||
}
|
||||
|
||||
static maze32_game_t initialized_game(maze32_maze_t* maze) {
|
||||
maze32_game_t game;
|
||||
maze32_game_config_t config = maze32_game_default_config();
|
||||
assert(config.movement_cooldown_ms == 150U);
|
||||
assert(maze32_game_init(&game, maze, &config) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
return game;
|
||||
}
|
||||
|
||||
static void assert_position(const maze32_game_t* game, uint8_t player, uint16_t x, uint16_t y) {
|
||||
assert(game->players[player].position.x == x);
|
||||
assert(game->players[player].position.y == y);
|
||||
}
|
||||
|
||||
static void test_movement_cooldown_and_ordering(void) {
|
||||
maze32_maze_t maze;
|
||||
open_test_grid(&maze);
|
||||
maze32_game_t game = initialized_game(&maze);
|
||||
uint32_t sequence = 0U;
|
||||
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
|
||||
100U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(sequence == 1U);
|
||||
maze32_game_command_result_t result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_PREMATURE);
|
||||
assert(result.command_sequence == 1U);
|
||||
assert(game.state_sequence == 0U);
|
||||
assert_position(&game, 0U, 1U, 1U);
|
||||
|
||||
maze32_game_set_phase(&game, MAZE32_GAME_PHASE_ACTIVE);
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
|
||||
100U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(sequence == 2U);
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_WEST,
|
||||
100U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(sequence == 3U);
|
||||
result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(result.command_sequence == 2U && result.state_sequence == 1U);
|
||||
assert_position(&game, 0U, 2U, 1U);
|
||||
assert_position(&game, 1U, 6U, 6U);
|
||||
result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(result.command_sequence == 3U && result.state_sequence == 2U);
|
||||
assert_position(&game, 1U, 5U, 6U);
|
||||
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_SOUTH,
|
||||
200U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_RATE_LIMITED);
|
||||
assert(game.state_sequence == 2U && game.players[0].last_move_ms == 100U);
|
||||
assert_position(&game, 0U, 2U, 1U);
|
||||
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_SOUTH,
|
||||
250U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert_position(&game, 0U, 2U, 2U);
|
||||
assert(game.players[0].accepted_moves == 2U);
|
||||
|
||||
uint16_t current = index_of(2U, 2U);
|
||||
maze.passages[current] &= (uint8_t)~MAZE32_PASSAGE_EAST;
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
|
||||
400U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_BLOCKED);
|
||||
assert(game.state_sequence == 3U && game.players[0].last_move_ms == 250U);
|
||||
assert_position(&game, 0U, 2U, 2U);
|
||||
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST,
|
||||
200U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_RATE_LIMITED);
|
||||
assert_position(&game, 0U, 2U, 2U);
|
||||
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_OBSERVER, MAZE32_GAME_DIRECTION_EAST,
|
||||
500U, &sequence) == MAZE32_GAME_RESULT_READ_ONLY);
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, (maze32_game_direction_t)99,
|
||||
500U, &sequence) == MAZE32_GAME_RESULT_INVALID_ARGUMENT);
|
||||
assert(game.state_sequence == 3U);
|
||||
printf("game movement, cooldown, and FIFO ordering: PASS (commands 1-7, states 0-3)\n");
|
||||
}
|
||||
|
||||
static void test_bounded_queue(void) {
|
||||
maze32_maze_t maze;
|
||||
open_test_grid(&maze);
|
||||
maze32_game_t game = initialized_game(&maze);
|
||||
uint32_t sequence = 0U;
|
||||
for (uint32_t index = 0U; index < MAZE32_GAME_COMMAND_CAPACITY; ++index) {
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE,
|
||||
MAZE32_GAME_DIRECTION_EAST, index,
|
||||
&sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(sequence == index + 1U);
|
||||
}
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
|
||||
20U, &sequence) == MAZE32_GAME_RESULT_QUEUE_FULL);
|
||||
assert(game.next_command_sequence == MAZE32_GAME_COMMAND_CAPACITY + 1U);
|
||||
assert(game.state_sequence == 0U);
|
||||
printf("game bounded command queue: PASS (%u entries)\n", MAZE32_GAME_COMMAND_CAPACITY);
|
||||
}
|
||||
|
||||
static void test_shared_cell_meeting(void) {
|
||||
maze32_maze_t maze;
|
||||
open_test_grid(&maze);
|
||||
maze.starts[1] = (maze32_cell_t){2U, 1U};
|
||||
maze32_game_t game = initialized_game(&maze);
|
||||
maze32_game_set_phase(&game, MAZE32_GAME_PHASE_ACTIVE);
|
||||
uint32_t sequence = 0U;
|
||||
assert(maze32_game_enqueue_move(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST,
|
||||
10U, &sequence) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
maze32_game_command_result_t result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(result.meeting == 1U && result.meeting_sequence == 1U);
|
||||
assert_position(&game, 0U, 2U, 1U);
|
||||
assert_position(&game, 1U, 2U, 1U);
|
||||
result = maze32_game_process_next(&game);
|
||||
assert(result.result == MAZE32_GAME_RESULT_QUEUE_EMPTY);
|
||||
assert(result.meeting == 0U && result.meeting_sequence == 1U);
|
||||
printf("game shared-cell meeting event: PASS (one event)\n");
|
||||
}
|
||||
|
||||
static int view_has_cell(const maze32_view_t* view, uint16_t cell_index) {
|
||||
for (uint16_t index = 0U; index < view->cell_count; ++index) {
|
||||
if (view->cells[index].cell_index == cell_index) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int view_has_object(const maze32_view_t* view, maze32_view_object_kind_t kind,
|
||||
uint16_t cell_index) {
|
||||
for (uint8_t index = 0U; index < view->object_count; ++index) {
|
||||
if (view->objects[index].kind == kind && view->objects[index].cell_index == cell_index) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void serialize_and_inspect(const maze32_view_t* view, int expect_hidden) {
|
||||
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
|
||||
size_t chunks = maze32_game_view_chunk_count(view);
|
||||
int saw_last_cell = 0;
|
||||
int saw_hidden_key = 0;
|
||||
int saw_players = 0;
|
||||
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
|
||||
size_t written = 0U;
|
||||
assert(maze32_game_serialize_view_chunk(view, chunk, payload, sizeof(payload), &written) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(written < sizeof(payload));
|
||||
assert(payload[written] == '\0');
|
||||
assert(strstr(payload, "{\"v\":1,\"type\":\"view\",\"seq\":") == payload);
|
||||
assert(strstr(payload, "\"phase\":\"countdown\"") != NULL);
|
||||
saw_last_cell |= strstr(payload, "[63,") != NULL;
|
||||
saw_hidden_key |= strstr(payload, "[\"key\",1,0,63]") != NULL;
|
||||
saw_players |= strstr(payload, "\"players\":[") != NULL;
|
||||
if (expect_hidden) {
|
||||
assert(strstr(payload, "\"opponent\":63") == NULL);
|
||||
}
|
||||
}
|
||||
if (expect_hidden) {
|
||||
assert(!saw_last_cell && !saw_hidden_key && !saw_players);
|
||||
} else {
|
||||
assert(saw_last_cell && saw_hidden_key && saw_players);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_visibility_and_serialization(void) {
|
||||
maze32_maze_t maze;
|
||||
open_test_grid(&maze);
|
||||
maze32_game_t game = initialized_game(&maze);
|
||||
maze32_view_t view;
|
||||
|
||||
game.players[0].position = (maze32_cell_t){3U, 3U};
|
||||
game.players[1].position = (maze32_cell_t){4U, 4U};
|
||||
maze.passages[index_of(3U, 3U)] = 0U;
|
||||
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U, &view) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(view.cell_count == 49U);
|
||||
assert(view_has_cell(&view, index_of(6U, 6U)));
|
||||
assert(view.opponent_visible && view.opponent_cell_index == index_of(4U, 4U));
|
||||
|
||||
game.players[0].position = (maze32_cell_t){0U, 0U};
|
||||
game.players[1].position = (maze32_cell_t){7U, 7U};
|
||||
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U, &view) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(view.cell_count == 16U);
|
||||
assert(!view.opponent_visible && view.opponent_cell_index == UINT16_MAX);
|
||||
assert(view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(2U, 1U)));
|
||||
assert(!view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(7U, 7U)));
|
||||
serialize_and_inspect(&view, 1);
|
||||
|
||||
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_TWO, 3U, &view) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(view.cell_count == 16U && view.self_cell_index == index_of(7U, 7U));
|
||||
assert(!view.opponent_visible && !view_has_cell(&view, index_of(0U, 0U)));
|
||||
assert(view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(7U, 7U)));
|
||||
assert(!view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(2U, 1U)));
|
||||
char player_two_payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
|
||||
for (size_t chunk = 0U; chunk < maze32_game_view_chunk_count(&view); ++chunk) {
|
||||
size_t written = 0U;
|
||||
assert(maze32_game_serialize_view_chunk(&view, chunk, player_two_payload,
|
||||
sizeof(player_two_payload),
|
||||
&written) == MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(strstr(player_two_payload, "\"role\":\"player2\"") != NULL);
|
||||
assert(strstr(player_two_payload, "\"opponent\":0") == NULL);
|
||||
assert(strstr(player_two_payload, "[\"key\",0,0,10]") == NULL);
|
||||
}
|
||||
|
||||
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U, &view) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(view.cell_count == 49U);
|
||||
assert(!view_has_cell(&view, index_of(7U, 7U)));
|
||||
|
||||
game.players[0].position = (maze32_cell_t){3U, 3U};
|
||||
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U, &view) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(view.cell_count == 64U);
|
||||
assert(view.opponent_visible);
|
||||
|
||||
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, 0U, &view) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(view.cell_count == 64U);
|
||||
assert(view.object_count == 10U);
|
||||
assert(view.observer_player_cell_indices[0] == index_of(3U, 3U));
|
||||
assert(view.observer_player_cell_indices[1] == index_of(7U, 7U));
|
||||
assert(view.phase == MAZE32_GAME_PHASE_COUNTDOWN);
|
||||
serialize_and_inspect(&view, 0);
|
||||
printf("game visibility and payload security: PASS (r3=49/16, r6=64/49, observer=64)\n");
|
||||
}
|
||||
|
||||
static void test_default_map_chunk_bounds(void) {
|
||||
maze32_maze_t maze;
|
||||
maze32_maze_workspace_t workspace;
|
||||
maze32_maze_config_t maze_config = maze32_maze_default_config();
|
||||
maze32_maze_report_t report;
|
||||
assert(maze32_maze_generate(&maze, &workspace, &maze_config, UINT32_C(0x5a320006), &report) ==
|
||||
MAZE32_MAZE_OK);
|
||||
maze32_game_t game = initialized_game(&maze);
|
||||
maze32_view_t view;
|
||||
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, 0U, &view) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
assert(view.cell_count == 8192U && view.object_count == 30U);
|
||||
size_t chunks = maze32_game_view_chunk_count(&view);
|
||||
assert(chunks == 345U);
|
||||
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
|
||||
size_t largest_chunk = 0U;
|
||||
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
|
||||
size_t written = 0U;
|
||||
assert(maze32_game_serialize_view_chunk(&view, chunk, payload, sizeof(payload), &written) ==
|
||||
MAZE32_GAME_RESULT_ACCEPTED);
|
||||
if (written > largest_chunk) {
|
||||
largest_chunk = written;
|
||||
}
|
||||
}
|
||||
assert(largest_chunk < MAZE32_GAME_VIEW_CHUNK_BYTES);
|
||||
printf("game default observer serialization: PASS (%u chunks, maximum %u bytes)\n",
|
||||
(unsigned int)chunks, (unsigned int)largest_chunk);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_movement_cooldown_and_ordering();
|
||||
test_bounded_queue();
|
||||
test_shared_cell_meeting();
|
||||
test_visibility_and_serialization();
|
||||
test_default_map_chunk_bounds();
|
||||
puts("game core tests: PASS");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user