#include "maze32_game.h" #include #include #include #define MAZE32_GAME_NO_OWNER UINT8_MAX static int role_is_player(maze32_game_role_t role) { return role == MAZE32_GAME_ROLE_PLAYER_ONE || role == MAZE32_GAME_ROLE_PLAYER_TWO; } static int direction_is_valid(maze32_game_direction_t direction) { return direction >= MAZE32_GAME_DIRECTION_NORTH && direction <= MAZE32_GAME_DIRECTION_WEST; } static maze32_passage_t direction_passage(maze32_game_direction_t direction) { static const maze32_passage_t PASSAGES[] = { MAZE32_PASSAGE_NORTH, MAZE32_PASSAGE_EAST, MAZE32_PASSAGE_SOUTH, MAZE32_PASSAGE_WEST, }; return PASSAGES[direction]; } static maze32_cell_t moved_cell(maze32_cell_t cell, maze32_game_direction_t direction) { if (direction == MAZE32_GAME_DIRECTION_NORTH) { cell.y--; } else if (direction == MAZE32_GAME_DIRECTION_EAST) { cell.x++; } else if (direction == MAZE32_GAME_DIRECTION_SOUTH) { cell.y++; } else { cell.x--; } return cell; } static uint16_t cell_index(const maze32_maze_t* maze, maze32_cell_t cell) { return (uint16_t)(cell.y * maze->config.width + cell.x); } static int valid_cell(const maze32_maze_t* maze, maze32_cell_t cell) { return cell.x < maze->config.width && cell.y < maze->config.height; } static int is_visible(maze32_cell_t center, maze32_cell_t cell, uint8_t radius) { uint16_t dx = center.x > cell.x ? center.x - cell.x : cell.x - center.x; uint16_t dy = center.y > cell.y ? center.y - cell.y : cell.y - center.y; return dx <= radius && dy <= radius; } maze32_game_config_t maze32_game_default_config(void) { const maze32_game_config_t config = {.movement_cooldown_ms = 150U}; return config; } maze32_game_result_t maze32_game_init(maze32_game_t* game, const maze32_maze_t* maze, const maze32_game_config_t* config) { if (game == NULL || maze == NULL || config == NULL || maze->config.width == 0U || maze->config.height == 0U || maze->config.width > MAZE32_MAZE_MAX_WIDTH || maze->config.height > MAZE32_MAZE_MAX_HEIGHT || maze->config.landmark_pairs > MAZE32_MAZE_MAX_LANDMARK_PAIRS || maze->config.artifact_pairs > MAZE32_MAZE_MAX_ARTIFACT_PAIRS || !valid_cell(maze, maze->starts[0]) || !valid_cell(maze, maze->starts[1])) { return MAZE32_GAME_RESULT_INVALID_ARGUMENT; } memset(game, 0, sizeof(*game)); game->maze = maze; game->config = *config; game->phase = MAZE32_GAME_PHASE_COUNTDOWN; game->players[0].position = maze->starts[0]; game->players[1].position = maze->starts[1]; game->next_command_sequence = 1U; return MAZE32_GAME_RESULT_ACCEPTED; } void maze32_game_set_phase(maze32_game_t* game, maze32_game_phase_t phase) { if (game != NULL && (phase == MAZE32_GAME_PHASE_COUNTDOWN || phase == MAZE32_GAME_PHASE_ACTIVE)) { game->phase = phase; } } maze32_game_result_t maze32_game_enqueue_move(maze32_game_t* game, maze32_game_role_t role, maze32_game_direction_t direction, uint64_t submitted_ms, uint32_t* command_sequence) { if (game == NULL || command_sequence == NULL || !direction_is_valid(direction)) { return MAZE32_GAME_RESULT_INVALID_ARGUMENT; } if (!role_is_player(role)) { return MAZE32_GAME_RESULT_READ_ONLY; } if (game->command_count >= MAZE32_GAME_COMMAND_CAPACITY) { return MAZE32_GAME_RESULT_QUEUE_FULL; } uint8_t tail = (uint8_t)((game->command_head + game->command_count) % MAZE32_GAME_COMMAND_CAPACITY); maze32_game_command_t* command = &game->commands[tail]; command->command_sequence = game->next_command_sequence++; command->role = role; command->direction = direction; command->submitted_ms = submitted_ms; game->command_count++; *command_sequence = command->command_sequence; return MAZE32_GAME_RESULT_ACCEPTED; } 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}; if (game == NULL) { return processed; } processed.state_sequence = game->state_sequence; processed.meeting_sequence = game->meeting_sequence; if (game->command_count == 0U) { processed.result = MAZE32_GAME_RESULT_QUEUE_EMPTY; return processed; } maze32_game_command_t command = game->commands[game->command_head]; game->command_head = (uint8_t)((game->command_head + 1U) % MAZE32_GAME_COMMAND_CAPACITY); game->command_count--; processed.command_sequence = command.command_sequence; if (game->phase != MAZE32_GAME_PHASE_ACTIVE) { processed.result = MAZE32_GAME_RESULT_PREMATURE; return processed; } uint8_t player_index = (uint8_t)command.role; maze32_game_player_t* player = &game->players[player_index]; if (player->has_moved && (command.submitted_ms < player->last_move_ms || command.submitted_ms - player->last_move_ms < game->config.movement_cooldown_ms)) { processed.result = MAZE32_GAME_RESULT_RATE_LIMITED; return processed; } maze32_passage_t passage = direction_passage(command.direction); if (!maze32_maze_has_passage(game->maze, player->position, passage)) { processed.result = MAZE32_GAME_RESULT_BLOCKED; return processed; } player->position = moved_cell(player->position, command.direction); player->last_move_ms = command.submitted_ms; player->accepted_moves++; player->has_moved = 1U; game->state_sequence++; processed.result = MAZE32_GAME_RESULT_ACCEPTED; processed.state_sequence = game->state_sequence; uint8_t opponent_index = (uint8_t)(1U - player_index); if (player->position.x == game->players[opponent_index].position.x && player->position.y == game->players[opponent_index].position.y) { game->meeting_sequence++; processed.meeting = 1U; processed.meeting_sequence = game->meeting_sequence; } return processed; } static void add_object(const maze32_game_t* game, maze32_view_t* view, maze32_cell_t cell, maze32_view_object_kind_t kind, uint8_t owner, uint8_t variant, maze32_cell_t center, uint8_t radius, int reveal_all) { if (view->object_count >= MAZE32_GAME_MAX_OBJECTS || (!reveal_all && !is_visible(center, cell, radius))) { return; } maze32_view_object_t* object = &view->objects[view->object_count++]; object->cell_index = cell_index(game->maze, cell); object->kind = kind; object->owner = owner; object->variant = variant; } static void project_objects(const maze32_game_t* game, maze32_view_t* view, maze32_cell_t center, uint8_t radius, int reveal_all) { for (uint8_t owner = 0U; owner < MAZE32_GAME_PLAYER_COUNT; ++owner) { add_object(game, view, game->maze->keys[owner], MAZE32_VIEW_OBJECT_KEY, owner, 0U, center, radius, reveal_all); add_object(game, view, game->maze->exits[owner], MAZE32_VIEW_OBJECT_EXIT, owner, 0U, center, radius, reveal_all); add_object(game, view, game->maze->compasses[owner], MAZE32_VIEW_OBJECT_COMPASS, owner, 0U, center, radius, reveal_all); } for (uint8_t pair = 0U; pair < game->maze->config.landmark_pairs; ++pair) { for (uint8_t side = 0U; side < 2U; ++side) { add_object(game, view, game->maze->landmarks[pair].cells[side], MAZE32_VIEW_OBJECT_LANDMARK, MAZE32_GAME_NO_OWNER, game->maze->landmarks[pair].kind, center, radius, reveal_all); } } for (uint8_t pair = 0U; pair < game->maze->config.artifact_pairs; ++pair) { for (uint8_t side = 0U; side < 2U; ++side) { add_object(game, view, game->maze->artifacts[pair][side], MAZE32_VIEW_OBJECT_VISION_ARTIFACT, MAZE32_GAME_NO_OWNER, pair, center, radius, reveal_all); } } } maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_game_role_t role, uint8_t radius, maze32_view_t* view) { if (game == NULL || game->maze == NULL || view == NULL || (!role_is_player(role) && role != MAZE32_GAME_ROLE_OBSERVER) || (role_is_player(role) && radius != 3U && radius != 6U)) { return MAZE32_GAME_RESULT_INVALID_ARGUMENT; } memset(view, 0, sizeof(*view)); view->role = role; view->phase = game->phase; view->state_sequence = game->state_sequence; view->meeting_sequence = game->meeting_sequence; view->opponent_cell_index = UINT16_MAX; int reveal_all = role == MAZE32_GAME_ROLE_OBSERVER; maze32_cell_t center = {0U, 0U}; if (role_is_player(role)) { uint8_t player_index = (uint8_t)role; center = game->players[player_index].position; view->self_cell_index = cell_index(game->maze, center); maze32_cell_t opponent = game->players[1U - player_index].position; if (is_visible(center, opponent, radius)) { view->opponent_visible = 1U; view->opponent_cell_index = cell_index(game->maze, opponent); } } else { view->self_cell_index = UINT16_MAX; view->observer_player_cell_indices[0] = cell_index(game->maze, game->players[0].position); view->observer_player_cell_indices[1] = cell_index(game->maze, game->players[1].position); } for (uint16_t y = 0U; y < game->maze->config.height; ++y) { for (uint16_t x = 0U; x < game->maze->config.width; ++x) { maze32_cell_t cell = {x, y}; if (reveal_all || is_visible(center, cell, radius)) { uint16_t index = cell_index(game->maze, cell); view->cells[view->cell_count].cell_index = index; view->cells[view->cell_count].passages = game->maze->passages[index]; view->cell_count++; } } } project_objects(game, view, center, radius, reveal_all); return MAZE32_GAME_RESULT_ACCEPTED; } size_t maze32_game_view_chunk_count(const maze32_view_t* view) { if (view == NULL) { return 0U; } size_t cell_chunks = (view->cell_count + MAZE32_GAME_VIEW_CELLS_PER_CHUNK - 1U) / MAZE32_GAME_VIEW_CELLS_PER_CHUNK; size_t object_chunks = (view->object_count + MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK - 1U) / MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK; return cell_chunks + object_chunks; } static int append_text(char* output, size_t output_size, size_t* offset, const char* format, ...) { va_list arguments; va_start(arguments, format); int count = vsnprintf(output + *offset, output_size - *offset, format, arguments); va_end(arguments); if (count < 0 || (size_t)count >= output_size - *offset) { return 0; } *offset += (size_t)count; return 1; } static const char* role_name(maze32_game_role_t role) { if (role == MAZE32_GAME_ROLE_PLAYER_ONE) { return "player1"; } if (role == MAZE32_GAME_ROLE_PLAYER_TWO) { return "player2"; } return "observer"; } static const char* object_name(maze32_view_object_kind_t kind) { static const char* const NAMES[] = {"key", "exit", "compass", "landmark", "vision"}; return NAMES[kind]; } static const char* phase_name(maze32_game_phase_t phase) { return phase == MAZE32_GAME_PHASE_ACTIVE ? "active" : "countdown"; } maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view, size_t chunk_index, char* output, size_t output_size, size_t* written) { if (view == NULL || output == NULL || written == NULL || output_size == 0U || view->cell_count > MAZE32_MAZE_MAX_CELLS || view->object_count > MAZE32_GAME_MAX_OBJECTS) { return MAZE32_GAME_RESULT_INVALID_ARGUMENT; } size_t total_chunks = maze32_game_view_chunk_count(view); if (chunk_index >= total_chunks) { return MAZE32_GAME_RESULT_INVALID_ARGUMENT; } size_t offset = 0U; if (!append_text(output, output_size, &offset, "{\"v\":1,\"type\":\"view\",\"seq\":%lu,\"payload\":{\"role\":\"%s\"," "\"phase\":\"%s\",\"meetingSequence\":%lu,\"chunk\":%u,\"chunks\":%u", (unsigned long)view->state_sequence, role_name(view->role), phase_name(view->phase), (unsigned long)view->meeting_sequence, (unsigned int)chunk_index, (unsigned int)total_chunks)) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } if (view->role == MAZE32_GAME_ROLE_OBSERVER) { if (!append_text(output, output_size, &offset, ",\"players\":[%u,%u]", view->observer_player_cell_indices[0], view->observer_player_cell_indices[1])) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } } else if (!append_text(output, output_size, &offset, ",\"self\":%u", (unsigned int)view->self_cell_index)) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } if (view->role != MAZE32_GAME_ROLE_OBSERVER && view->opponent_visible && !append_text(output, output_size, &offset, ",\"opponent\":%u", (unsigned int)view->opponent_cell_index)) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } if (!append_text(output, output_size, &offset, ",\"cells\":[")) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } size_t cell_chunks = (view->cell_count + MAZE32_GAME_VIEW_CELLS_PER_CHUNK - 1U) / MAZE32_GAME_VIEW_CELLS_PER_CHUNK; if (chunk_index < cell_chunks) { size_t first = chunk_index * MAZE32_GAME_VIEW_CELLS_PER_CHUNK; size_t end = first + MAZE32_GAME_VIEW_CELLS_PER_CHUNK; if (end > view->cell_count) { end = view->cell_count; } for (size_t index = first; index < end; ++index) { const maze32_view_cell_t* cell = &view->cells[index]; if (!append_text(output, output_size, &offset, "%s[%u,%u]", index == first ? "" : ",", cell->cell_index, cell->passages)) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } } } if (!append_text(output, output_size, &offset, "],\"objects\":[")) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } if (chunk_index >= cell_chunks) { size_t object_chunk = chunk_index - cell_chunks; size_t first = object_chunk * MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK; size_t end = first + MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK; if (end > view->object_count) { end = view->object_count; } for (size_t index = first; index < end; ++index) { const maze32_view_object_t* object = &view->objects[index]; if (!append_text(output, output_size, &offset, "%s[\"%s\",%u,%u,%u]", index == first ? "" : ",", object_name(object->kind), object->owner, object->variant, object->cell_index)) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } } } if (!append_text(output, output_size, &offset, "]}}")) { return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL; } *written = offset; return MAZE32_GAME_RESULT_ACCEPTED; } const char* maze32_game_result_name(maze32_game_result_t result) { static const char* const NAMES[] = {"accepted", "invalid_argument", "queue_full", "queue_empty", "read_only", "premature", "rate_limited", "blocked", "buffer_too_small"}; if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_BUFFER_TOO_SMALL) { return "unknown"; } return NAMES[result]; }