333 lines
15 KiB
C
333 lines
15 KiB
C
#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);
|
|
assert(strstr(payload, "\"width\":8,\"height\":8") != 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, &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, &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, &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);
|
|
}
|
|
|
|
game.players[0].vision_moves_remaining = 1U;
|
|
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
|
|
MAZE32_GAME_RESULT_ACCEPTED);
|
|
assert(view.cell_count == 49U);
|
|
assert(!view_has_cell(&view, index_of(7U, 7U)));
|
|
|
|
game.players[0].position = (maze32_cell_t){3U, 3U};
|
|
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
|
|
MAZE32_GAME_RESULT_ACCEPTED);
|
|
assert(view.cell_count == 64U);
|
|
assert(view.opponent_visible);
|
|
|
|
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &view) ==
|
|
MAZE32_GAME_RESULT_ACCEPTED);
|
|
assert(view.cell_count == 64U);
|
|
assert(view.object_count == 10U);
|
|
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);
|
|
game.phase = MAZE32_GAME_PHASE_FINISHED;
|
|
game.final_result = MAZE32_GAME_FINAL_DRAW;
|
|
game.state_sequence = UINT32_MAX;
|
|
game.meeting_sequence = UINT32_MAX;
|
|
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
|
|
game.players[player].vision_moves_remaining = MAZE32_GAME_VISION_MOVES;
|
|
game.players[player].compass_hint = MAZE32_GAME_COMPASS_NORTH_WEST;
|
|
game.players[player].exited = 1U;
|
|
game.exits[player].open = 1U;
|
|
game.has_exit_time[player] = 1U;
|
|
game.exit_times_ms[player] = UINT64_MAX - player;
|
|
}
|
|
maze32_view_t view;
|
|
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &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 == 688U);
|
|
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;
|
|
}
|