#include "maze32_game.h" #include #include #include 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, &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; }