feat(game): complete authoritative gameplay mechanics
This commit is contained in:
@@ -11,10 +11,15 @@ extern "C" {
|
||||
|
||||
#define MAZE32_GAME_PLAYER_COUNT 2U
|
||||
#define MAZE32_GAME_COMMAND_CAPACITY 16U
|
||||
#define MAZE32_GAME_KEY_COUNT 2U
|
||||
#define MAZE32_GAME_NO_KEY UINT8_MAX
|
||||
#define MAZE32_GAME_VISION_MOVES 10U
|
||||
#define MAZE32_GAME_DRAW_WINDOW_MS 200U
|
||||
#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 24U
|
||||
#define MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK 10U
|
||||
#define MAZE32_GAME_VIEW_CELLS_PER_CHUNK 12U
|
||||
#define MAZE32_GAME_VIEW_OBJECTS_PER_CHUNK 6U
|
||||
#define MAZE32_GAME_VIEW_CHUNK_BYTES 512U
|
||||
#define MAZE32_GAME_RAM_BUDGET_BYTES (40U * 1024U)
|
||||
#define MAZE32_GAME_RUNTIME_BUDGET_MS 50U
|
||||
@@ -22,6 +27,8 @@ extern "C" {
|
||||
typedef enum {
|
||||
MAZE32_GAME_PHASE_COUNTDOWN = 0,
|
||||
MAZE32_GAME_PHASE_ACTIVE,
|
||||
MAZE32_GAME_PHASE_FINISH_PENDING,
|
||||
MAZE32_GAME_PHASE_FINISHED,
|
||||
} maze32_game_phase_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -37,6 +44,30 @@ typedef enum {
|
||||
MAZE32_GAME_DIRECTION_WEST,
|
||||
} maze32_game_direction_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_GAME_COMMAND_MOVE = 0,
|
||||
MAZE32_GAME_COMMAND_DROP_KEY,
|
||||
} maze32_game_command_type_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_GAME_FINAL_NONE = 0,
|
||||
MAZE32_GAME_FINAL_PLAYER_ONE,
|
||||
MAZE32_GAME_FINAL_PLAYER_TWO,
|
||||
MAZE32_GAME_FINAL_DRAW,
|
||||
} maze32_game_final_result_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_GAME_COMPASS_NONE = 0,
|
||||
MAZE32_GAME_COMPASS_NORTH,
|
||||
MAZE32_GAME_COMPASS_NORTH_EAST,
|
||||
MAZE32_GAME_COMPASS_EAST,
|
||||
MAZE32_GAME_COMPASS_SOUTH_EAST,
|
||||
MAZE32_GAME_COMPASS_SOUTH,
|
||||
MAZE32_GAME_COMPASS_SOUTH_WEST,
|
||||
MAZE32_GAME_COMPASS_WEST,
|
||||
MAZE32_GAME_COMPASS_NORTH_WEST,
|
||||
} maze32_game_compass_direction_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_GAME_RESULT_ACCEPTED = 0,
|
||||
MAZE32_GAME_RESULT_INVALID_ARGUMENT,
|
||||
@@ -47,6 +78,9 @@ typedef enum {
|
||||
MAZE32_GAME_RESULT_RATE_LIMITED,
|
||||
MAZE32_GAME_RESULT_BLOCKED,
|
||||
MAZE32_GAME_RESULT_BUFFER_TOO_SMALL,
|
||||
MAZE32_GAME_RESULT_NO_KEY,
|
||||
MAZE32_GAME_RESULT_PLAYER_EXITED,
|
||||
MAZE32_GAME_RESULT_MATCH_FINISHED,
|
||||
} maze32_game_result_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -57,11 +91,31 @@ typedef struct {
|
||||
maze32_cell_t position;
|
||||
uint64_t last_move_ms;
|
||||
uint32_t accepted_moves;
|
||||
uint8_t carried_key;
|
||||
uint8_t vision_moves_remaining;
|
||||
maze32_game_compass_direction_t compass_hint;
|
||||
uint8_t has_moved;
|
||||
uint8_t exited;
|
||||
} maze32_game_player_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_GAME_KEY_GROUND = 0,
|
||||
MAZE32_GAME_KEY_CARRIED,
|
||||
} maze32_game_key_location_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_game_key_location_t location;
|
||||
maze32_cell_t ground_position;
|
||||
uint8_t carrier;
|
||||
} maze32_game_key_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t open;
|
||||
} maze32_game_exit_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t command_sequence;
|
||||
maze32_game_command_type_t type;
|
||||
maze32_game_role_t role;
|
||||
maze32_game_direction_t direction;
|
||||
uint64_t submitted_ms;
|
||||
@@ -73,6 +127,18 @@ typedef struct {
|
||||
uint32_t state_sequence;
|
||||
uint32_t meeting_sequence;
|
||||
uint8_t meeting;
|
||||
uint8_t moved;
|
||||
uint8_t dropped_key;
|
||||
uint8_t picked_key;
|
||||
uint8_t returned_keys;
|
||||
uint8_t activated_artifact;
|
||||
uint8_t opened_exit;
|
||||
uint8_t outward_exit;
|
||||
uint8_t finished;
|
||||
uint8_t key;
|
||||
uint8_t artifact;
|
||||
maze32_game_compass_direction_t compass_hint;
|
||||
maze32_game_final_result_t final_result;
|
||||
} maze32_game_command_result_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -80,12 +146,19 @@ typedef struct {
|
||||
maze32_game_config_t config;
|
||||
maze32_game_phase_t phase;
|
||||
maze32_game_player_t players[MAZE32_GAME_PLAYER_COUNT];
|
||||
maze32_game_key_t keys[MAZE32_GAME_KEY_COUNT];
|
||||
maze32_game_exit_t exits[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint8_t consumed_artifacts[MAZE32_GAME_MAX_ARTIFACTS];
|
||||
maze32_game_command_t commands[MAZE32_GAME_COMMAND_CAPACITY];
|
||||
uint8_t command_head;
|
||||
uint8_t command_count;
|
||||
uint32_t next_command_sequence;
|
||||
uint32_t state_sequence;
|
||||
uint32_t meeting_sequence;
|
||||
maze32_game_final_result_t final_result;
|
||||
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_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -113,10 +186,24 @@ typedef struct {
|
||||
maze32_game_phase_t phase;
|
||||
uint32_t state_sequence;
|
||||
uint32_t meeting_sequence;
|
||||
maze32_game_final_result_t final_result;
|
||||
uint16_t self_cell_index;
|
||||
uint16_t opponent_cell_index;
|
||||
uint16_t observer_player_cell_indices[MAZE32_GAME_PLAYER_COUNT];
|
||||
uint16_t cell_count;
|
||||
uint8_t self_carried_key;
|
||||
uint8_t self_vision_moves_remaining;
|
||||
maze32_game_compass_direction_t self_compass_hint;
|
||||
uint8_t self_exit_open;
|
||||
uint8_t self_exited;
|
||||
uint8_t opponent_carried_key;
|
||||
uint8_t observer_carried_keys[MAZE32_GAME_PLAYER_COUNT];
|
||||
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_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];
|
||||
uint8_t object_count;
|
||||
uint8_t opponent_visible;
|
||||
maze32_view_cell_t cells[MAZE32_MAZE_MAX_CELLS];
|
||||
@@ -130,9 +217,14 @@ void maze32_game_set_phase(maze32_game_t* game, maze32_game_phase_t 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);
|
||||
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);
|
||||
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);
|
||||
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,
|
||||
uint8_t radius, maze32_view_t* view);
|
||||
maze32_view_t* view);
|
||||
size_t maze32_game_view_chunk_count(const maze32_view_t* view);
|
||||
maze32_game_result_t maze32_game_serialize_view_chunk(const maze32_view_t* view, size_t chunk_index,
|
||||
char* output, size_t output_size,
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user