feat(observer): add observer mode and disconnect recovery

This commit is contained in:
2026-09-05 21:19:52 +03:00
parent d042c3ce90
commit ffdaa299bc
23 changed files with 988 additions and 43 deletions
+111 -9
View File
@@ -78,11 +78,13 @@ 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->phase_before_pause = MAZE32_GAME_PHASE_ACTIVE;
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->players[player].connected = 1U;
game->keys[player].location = MAZE32_GAME_KEY_GROUND;
game->keys[player].ground_position = maze->keys[player];
game->keys[player].carrier = MAZE32_GAME_NO_OWNER;
@@ -93,7 +95,13 @@ maze32_game_result_t maze32_game_init(maze32_game_t* game, const maze32_maze_t*
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_FINISHED) {
game->phase = phase;
if (phase == MAZE32_GAME_PHASE_ACTIVE &&
(!game->players[0].connected || !game->players[1].connected)) {
game->phase_before_pause = MAZE32_GAME_PHASE_ACTIVE;
game->phase = MAZE32_GAME_PHASE_PAUSED;
} else {
game->phase = phase;
}
}
}
@@ -139,7 +147,25 @@ maze32_game_result_t maze32_game_enqueue_drop_key(maze32_game_t* game, maze32_ga
}
int maze32_game_advance_time(maze32_game_t* game, uint64_t now_ms) {
if (game == NULL || game->phase != MAZE32_GAME_PHASE_FINISH_PENDING ||
if (game == NULL) {
return 0;
}
if (game->phase == MAZE32_GAME_PHASE_PAUSED) {
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
if (!game->players[player].connected &&
now_ms >= game->players[player].reconnect_deadline_ms) {
game->phase = MAZE32_GAME_PHASE_FINISHED;
game->final_result = MAZE32_GAME_FINAL_DRAW;
game->paused_started_ms = 0U;
game->command_head = 0U;
game->command_count = 0U;
game->state_sequence++;
return 1;
}
}
return 0;
}
if (game->phase != MAZE32_GAME_PHASE_FINISH_PENDING ||
game->first_exited_player >= MAZE32_GAME_PLAYER_COUNT) {
return 0;
}
@@ -154,6 +180,60 @@ int maze32_game_advance_time(maze32_game_t* game, uint64_t now_ms) {
return 1;
}
maze32_game_result_t maze32_game_set_player_connected(maze32_game_t* game, uint8_t player_index,
int connected, uint64_t now_ms) {
if (game == NULL || player_index >= MAZE32_GAME_PLAYER_COUNT) {
return MAZE32_GAME_RESULT_INVALID_ARGUMENT;
}
maze32_game_player_t* player = &game->players[player_index];
uint8_t normalized = connected ? 1U : 0U;
if (player->connected == normalized) {
return MAZE32_GAME_RESULT_ACCEPTED;
}
if (game->phase == MAZE32_GAME_PHASE_FINISHED) {
return MAZE32_GAME_RESULT_MATCH_FINISHED;
}
if (normalized) {
if (game->phase == MAZE32_GAME_PHASE_PAUSED && now_ms >= player->reconnect_deadline_ms) {
maze32_game_advance_time(game, now_ms);
return MAZE32_GAME_RESULT_MATCH_FINISHED;
}
player->connected = 1U;
player->reconnect_deadline_ms = 0U;
if (game->phase == MAZE32_GAME_PHASE_PAUSED && game->players[0].connected &&
game->players[1].connected) {
if (game->phase_before_pause == MAZE32_GAME_PHASE_FINISH_PENDING &&
game->first_exited_player < MAZE32_GAME_PLAYER_COUNT &&
now_ms > game->paused_started_ms) {
uint64_t pause_duration_ms = now_ms - game->paused_started_ms;
uint64_t* exit_time_ms = &game->exit_times_ms[game->first_exited_player];
*exit_time_ms = *exit_time_ms > UINT64_MAX - pause_duration_ms
? UINT64_MAX
: *exit_time_ms + pause_duration_ms;
}
game->phase = game->phase_before_pause;
game->paused_started_ms = 0U;
}
game->state_sequence++;
return MAZE32_GAME_RESULT_ACCEPTED;
}
player->connected = 0U;
player->reconnect_deadline_ms = now_ms > UINT64_MAX - MAZE32_GAME_RECONNECT_TIMEOUT_MS
? UINT64_MAX
: now_ms + MAZE32_GAME_RECONNECT_TIMEOUT_MS;
if (game->phase == MAZE32_GAME_PHASE_COUNTDOWN || game->phase == MAZE32_GAME_PHASE_ACTIVE ||
game->phase == MAZE32_GAME_PHASE_FINISH_PENDING) {
game->phase_before_pause = game->phase;
game->phase = MAZE32_GAME_PHASE_PAUSED;
game->paused_started_ms = now_ms;
game->command_head = 0U;
game->command_count = 0U;
}
game->state_sequence++;
return MAZE32_GAME_RESULT_ACCEPTED;
}
static void accept_player_move(maze32_game_player_t* player, uint64_t submitted_ms) {
player->last_move_ms = submitted_ms;
player->accepted_moves++;
@@ -496,6 +576,7 @@ maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_
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_connected[player] = game->players[player].connected;
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];
@@ -517,12 +598,24 @@ maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_
return MAZE32_GAME_RESULT_ACCEPTED;
}
static size_t cell_chunk_count(const maze32_view_t* view) {
if (view->role != MAZE32_GAME_ROLE_OBSERVER || view->cell_count == 0U) {
return (view->cell_count + MAZE32_GAME_VIEW_CELLS_PER_CHUNK - 1U) /
MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
}
if (view->cell_count <= MAZE32_GAME_OBSERVER_FIRST_CHUNK_CELLS) {
return 1U;
}
size_t remaining = view->cell_count - MAZE32_GAME_OBSERVER_FIRST_CHUNK_CELLS;
return 1U +
(remaining + MAZE32_GAME_VIEW_CELLS_PER_CHUNK - 1U) / MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
}
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 cell_chunks = cell_chunk_count(view);
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;
@@ -556,7 +649,8 @@ static const char* object_name(maze32_view_object_kind_t kind) {
}
static const char* phase_name(maze32_game_phase_t phase) {
static const char* const NAMES[] = {"countdown", "active", "finish_pending", "finished"};
static const char* const NAMES[] = {"countdown", "active", "finish_pending", "paused",
"finished"};
return NAMES[phase];
}
@@ -605,11 +699,12 @@ static int append_observer_status(const maze32_view_t* view, char* output, size_
}
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",
"%s{\"cell\":%u,\"visionMoves\":%u,\"hint\":\"%s\",\"exited\":%u,"
"\"connected\":%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]) ||
view->observer_exited[player], view->observer_connected[player]) ||
!append_nullable_key(output, output_size, offset, "key",
view->observer_carried_keys[player]) ||
!append_text(output, output_size, offset, "}")) {
@@ -669,11 +764,18 @@ maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view,
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;
size_t cell_chunks = cell_chunk_count(view);
if (chunk_index < cell_chunks) {
size_t first = chunk_index * MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
if (view->role == MAZE32_GAME_ROLE_OBSERVER) {
first = chunk_index == 0U ? 0U
: MAZE32_GAME_OBSERVER_FIRST_CHUNK_CELLS +
(chunk_index - 1U) * MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
}
size_t end = first + MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
if (view->role == MAZE32_GAME_ROLE_OBSERVER && chunk_index == 0U) {
end = MAZE32_GAME_OBSERVER_FIRST_CHUNK_CELLS;
}
if (end > view->cell_count) {
end = view->cell_count;
}