feat(observer): add observer mode and disconnect recovery
This commit is contained in:
@@ -15,10 +15,12 @@ extern "C" {
|
||||
#define MAZE32_GAME_NO_KEY UINT8_MAX
|
||||
#define MAZE32_GAME_VISION_MOVES 10U
|
||||
#define MAZE32_GAME_DRAW_WINDOW_MS 200U
|
||||
#define MAZE32_GAME_RECONNECT_TIMEOUT_MS 60000U
|
||||
#define MAZE32_GAME_MAX_ARTIFACTS (2U * MAZE32_MAZE_MAX_ARTIFACT_PAIRS)
|
||||
#define MAZE32_GAME_MAX_OBJECTS \
|
||||
(6U + 2U * MAZE32_MAZE_MAX_LANDMARK_PAIRS + 2U * MAZE32_MAZE_MAX_ARTIFACT_PAIRS)
|
||||
#define MAZE32_GAME_VIEW_CELLS_PER_CHUNK 12U
|
||||
#define MAZE32_GAME_OBSERVER_FIRST_CHUNK_CELLS 8U
|
||||
#define MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK 6U
|
||||
#define MAZE32_GAME_VIEW_CHUNK_BYTES 512U
|
||||
#define MAZE32_GAME_RAM_BUDGET_BYTES (40U * 1024U)
|
||||
@@ -28,6 +30,7 @@ typedef enum {
|
||||
MAZE32_GAME_PHASE_COUNTDOWN = 0,
|
||||
MAZE32_GAME_PHASE_ACTIVE,
|
||||
MAZE32_GAME_PHASE_FINISH_PENDING,
|
||||
MAZE32_GAME_PHASE_PAUSED,
|
||||
MAZE32_GAME_PHASE_FINISHED,
|
||||
} maze32_game_phase_t;
|
||||
|
||||
@@ -96,6 +99,8 @@ typedef struct {
|
||||
maze32_game_compass_direction_t compass_hint;
|
||||
uint8_t has_moved;
|
||||
uint8_t exited;
|
||||
uint8_t connected;
|
||||
uint64_t reconnect_deadline_ms;
|
||||
} maze32_game_player_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -159,6 +164,8 @@ typedef struct {
|
||||
uint8_t first_exited_player;
|
||||
uint8_t has_exit_time[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint64_t exit_times_ms[MAZE32_GAME_PLAYER_COUNT];
|
||||
maze32_game_phase_t phase_before_pause;
|
||||
uint64_t paused_started_ms;
|
||||
} maze32_game_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -203,6 +210,7 @@ typedef struct {
|
||||
uint8_t observer_vision_moves_remaining[MAZE32_GAME_PLAYER_COUNT];
|
||||
maze32_game_compass_direction_t observer_compass_hints[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint8_t observer_exited[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint8_t observer_connected[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint8_t observer_exit_open[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint8_t observer_has_exit_time[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint64_t observer_exit_times_ms[MAZE32_GAME_PLAYER_COUNT];
|
||||
@@ -224,6 +232,8 @@ maze32_game_result_t maze32_game_enqueue_drop_key(maze32_game_t* game, maze32_ga
|
||||
uint32_t* command_sequence);
|
||||
maze32_game_command_result_t maze32_game_process_next(maze32_game_t* game);
|
||||
int maze32_game_advance_time(maze32_game_t* game, uint64_t now_ms);
|
||||
maze32_game_result_t maze32_game_set_player_connected(maze32_game_t* game, uint8_t player_index,
|
||||
int connected, uint64_t now_ms);
|
||||
uint8_t maze32_game_player_view_radius(const maze32_game_t* game, uint8_t player_index);
|
||||
maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_game_role_t role,
|
||||
maze32_view_t* view);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user