feat(game): complete authoritative gameplay mechanics

This commit is contained in:
2026-09-04 12:25:37 +03:00
parent 115a3bd0a1
commit 9339578dc9
13 changed files with 8795 additions and 109 deletions
+419 -75
View File
@@ -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 const maze32_passage_t PASSAGES[] = {
MAZE32_PASSAGE_NORTH,
MAZE32_PASSAGE_EAST,
MAZE32_PASSAGE_SOUTH,
MAZE32_PASSAGE_WEST,
};
static const maze32_passage_t PASSAGES[] = {MAZE32_PASSAGE_NORTH, MAZE32_PASSAGE_EAST,
MAZE32_PASSAGE_SOUTH, MAZE32_PASSAGE_WEST};
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;
}
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) {
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;
}
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) {
const maze32_game_config_t config = {.movement_cooldown_ms = 150U};
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->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;
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;
}
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)) {
if (game != NULL && phase >= MAZE32_GAME_PHASE_COUNTDOWN &&
phase <= MAZE32_GAME_PHASE_FINISHED) {
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)) {
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,
uint64_t submitted_ms, uint32_t* command_sequence) {
if (game == NULL || command_sequence == NULL ||
(type == MAZE32_GAME_COMMAND_MOVE && !direction_is_valid(direction))) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
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);
maze32_game_command_t* command = &game->commands[tail];
command->command_sequence = game->next_command_sequence++;
command->type = type;
command->role = role;
command->direction = direction;
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;
}
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 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) {
return processed;
}
processed.state_sequence = game->state_sequence;
processed.meeting_sequence = game->meeting_sequence;
processed.final_result = game->final_result;
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) {
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;
return processed;
}
uint8_t player_index = (uint8_t)command.role;
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 &&
(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;
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);
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);
accept_player_move(player, command.submitted_ms);
processed.moved = 1U;
resolve_cell_entry(game, player_index, &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;
}
processed.meeting_sequence = game->meeting_sequence;
processed.final_result = game->final_result;
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,
maze32_view_object_kind_t kind, uint8_t owner, uint8_t variant,
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,
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);
if (game->keys[owner].location == MAZE32_GAME_KEY_GROUND) {
add_object(game, view, game->keys[owner].ground_position, MAZE32_VIEW_OBJECT_KEY, owner,
0U, center, 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,
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 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);
uint8_t artifact = (uint8_t)(pair * 2U + side);
if (!game->consumed_artifacts[artifact]) {
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) {
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)) {
(!role_is_player(role) && role != MAZE32_GAME_ROLE_OBSERVER)) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
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->state_sequence = game->state_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;
int reveal_all = role == MAZE32_GAME_ROLE_OBSERVER;
uint8_t radius = 0U;
maze32_cell_t center = {0U, 0U};
if (role_is_player(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);
maze32_cell_t opponent = game->players[1U - player_index].position;
if (is_visible(center, opponent, radius)) {
view->self_carried_key = player.carried_key;
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_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 {
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 (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
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) {
@@ -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) {
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,
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) {
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;
}
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;
if (!append_text(output, output_size, &offset,
"{\"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),
phase_name(view->phase), (unsigned long)view->meeting_sequence,
(unsigned int)chunk_index, (unsigned int)total_chunks)) {
phase_name(view->phase), final_result_name(view->final_result),
(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;
int status_appended = 1;
if (chunk_index == 0U) {
status_appended = view->role == MAZE32_GAME_ROLE_OBSERVER
? append_observer_status(view, output, output_size, &offset)
: append_player_status(view, output, output_size, &offset);
}
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\":[")) {
if (!status_appended || !append_text(output, output_size, &offset, ",\"cells\":[")) {
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\":[")) {
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;
@@ -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) {
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]",
index == first ? "" : ",", object_name(object->kind), object->owner,
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) {
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) {
static const char* const NAMES[] = {
"accepted", "invalid_argument", "queue_full", "queue_empty",
"read_only", "premature", "rate_limited", "blocked",
"buffer_too_small", "no_key", "player_exited", "match_finished",
};
if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_MATCH_FINISHED) {
return "unknown";
}
return NAMES[result];