957 lines
41 KiB
C
957 lines
41 KiB
C
#include "maze32_game.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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_passage_t opposite_passage(maze32_passage_t passage) {
|
|
if (passage == MAZE32_PASSAGE_NORTH) {
|
|
return MAZE32_PASSAGE_SOUTH;
|
|
}
|
|
if (passage == MAZE32_PASSAGE_EAST) {
|
|
return MAZE32_PASSAGE_WEST;
|
|
}
|
|
if (passage == MAZE32_PASSAGE_SOUTH) {
|
|
return MAZE32_PASSAGE_NORTH;
|
|
}
|
|
return MAZE32_PASSAGE_EAST;
|
|
}
|
|
|
|
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 cells_equal(maze32_cell_t first, maze32_cell_t second) {
|
|
return first.x == second.x && first.y == second.y;
|
|
}
|
|
|
|
static uint8_t game_passages(const maze32_game_t* game, maze32_cell_t cell) {
|
|
return (uint8_t)(game->maze->passages[cell_index(game->maze, cell)] |
|
|
game->opened_passages[cell_index(game->maze, cell)]);
|
|
}
|
|
|
|
static int game_has_passage(const maze32_game_t* game, maze32_cell_t cell,
|
|
maze32_passage_t passage) {
|
|
return (game_passages(game, cell) & passage) != 0U;
|
|
}
|
|
|
|
static uint16_t trail_edge_index(const maze32_maze_t* maze, maze32_cell_t cell,
|
|
maze32_passage_t passage) {
|
|
uint16_t horizontal_count = (uint16_t)(maze->config.height * (maze->config.width - 1U));
|
|
if (passage == MAZE32_PASSAGE_EAST) {
|
|
return (uint16_t)(cell.y * (maze->config.width - 1U) + cell.x);
|
|
}
|
|
return (uint16_t)(horizontal_count + cell.y * maze->config.width + cell.x);
|
|
}
|
|
|
|
static uint8_t trail_owner_mask(const maze32_game_t* game, uint16_t edge_index) {
|
|
uint8_t byte = game->trail_owners[edge_index / 4U];
|
|
return (uint8_t)((byte >> ((edge_index % 4U) * 2U)) & 0x03U);
|
|
}
|
|
|
|
static void mark_trail_edge(maze32_game_t* game, uint8_t player_index, maze32_cell_t origin,
|
|
maze32_game_direction_t direction) {
|
|
maze32_cell_t canonical = origin;
|
|
maze32_passage_t passage = direction_passage(direction);
|
|
if (direction == MAZE32_GAME_DIRECTION_NORTH) {
|
|
canonical.y--;
|
|
passage = MAZE32_PASSAGE_SOUTH;
|
|
} else if (direction == MAZE32_GAME_DIRECTION_WEST) {
|
|
canonical.x--;
|
|
passage = MAZE32_PASSAGE_EAST;
|
|
}
|
|
uint16_t edge_index = trail_edge_index(game->maze, canonical, passage);
|
|
uint8_t shift = (uint8_t)((edge_index % 4U) * 2U);
|
|
game->trail_owners[edge_index / 4U] |= (uint8_t)((1U << player_index) << shift);
|
|
}
|
|
|
|
static void reset_perseverance(maze32_game_player_t* player) {
|
|
player->perseverance_hits = 0U;
|
|
player->perseverance_last_impact_ms = 0U;
|
|
}
|
|
|
|
static uint8_t record_wall_impact(maze32_game_player_t* player, maze32_cell_t cell,
|
|
maze32_game_direction_t direction, uint64_t submitted_ms) {
|
|
if (player->perseverance_hits == 0U || !cells_equal(player->perseverance_cell, cell) ||
|
|
player->perseverance_direction != direction ||
|
|
submitted_ms < player->perseverance_last_impact_ms ||
|
|
submitted_ms - player->perseverance_last_impact_ms >= MAZE32_GAME_BREAKTHROUGH_TIMEOUT_MS) {
|
|
player->perseverance_cell = cell;
|
|
player->perseverance_direction = direction;
|
|
player->perseverance_hits = 1U;
|
|
} else {
|
|
player->perseverance_hits++;
|
|
}
|
|
player->perseverance_last_impact_ms = submitted_ms;
|
|
return player->perseverance_hits;
|
|
}
|
|
|
|
static void reset_all_perseverance(maze32_game_t* game) {
|
|
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
|
|
reset_perseverance(&game->players[player]);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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->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;
|
|
}
|
|
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_FINISHED) {
|
|
if (phase != MAZE32_GAME_PHASE_ACTIVE) {
|
|
reset_all_perseverance(game);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
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->type = type;
|
|
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_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) {
|
|
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;
|
|
reset_all_perseverance(game);
|
|
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;
|
|
}
|
|
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;
|
|
reset_all_perseverance(game);
|
|
game->state_sequence++;
|
|
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;
|
|
reset_perseverance(player);
|
|
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;
|
|
reset_all_perseverance(game);
|
|
}
|
|
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++;
|
|
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;
|
|
reset_all_perseverance(game);
|
|
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,
|
|
.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 (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;
|
|
}
|
|
if (direction_leaves_map(game->maze, player->position, command.direction)) {
|
|
reset_perseverance(player);
|
|
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_cell_t origin = player->position;
|
|
maze32_passage_t passage = direction_passage(command.direction);
|
|
if (!game_has_passage(game, player->position, passage)) {
|
|
processed.breakthrough_hits = record_wall_impact(
|
|
player, player->position, command.direction, command.submitted_ms);
|
|
if (processed.breakthrough_hits < MAZE32_GAME_BREAKTHROUGH_HITS) {
|
|
processed.result = MAZE32_GAME_RESULT_BLOCKED;
|
|
return processed;
|
|
}
|
|
maze32_cell_t destination = moved_cell(player->position, command.direction);
|
|
if (!valid_cell(game->maze, destination)) {
|
|
reset_perseverance(player);
|
|
processed.result = MAZE32_GAME_RESULT_BLOCKED;
|
|
return processed;
|
|
}
|
|
game->opened_passages[cell_index(game->maze, player->position)] |= passage;
|
|
game->opened_passages[cell_index(game->maze, destination)] |= opposite_passage(passage);
|
|
processed.breakthrough_opened = 1U;
|
|
reset_perseverance(player);
|
|
}
|
|
if (game_has_passage(game, player->position, passage)) {
|
|
reset_perseverance(player);
|
|
} else {
|
|
processed.result = MAZE32_GAME_RESULT_BLOCKED;
|
|
return processed;
|
|
}
|
|
player->position = moved_cell(player->position, command.direction);
|
|
mark_trail_edge(game, player_index, origin, command.direction);
|
|
accept_player_move(player, command.submitted_ms);
|
|
processed.moved = 1U;
|
|
resolve_cell_entry(game, player_index, &processed);
|
|
}
|
|
game->state_sequence++;
|
|
processed.result = MAZE32_GAME_RESULT_ACCEPTED;
|
|
processed.state_sequence = game->state_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) {
|
|
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) {
|
|
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);
|
|
}
|
|
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) {
|
|
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,
|
|
maze32_view_t* view) {
|
|
if (game == NULL || game->maze == NULL || view == NULL ||
|
|
(!role_is_player(role) && role != MAZE32_GAME_ROLE_OBSERVER)) {
|
|
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->final_result = game->final_result;
|
|
view->width = game->maze->config.width;
|
|
view->height = game->maze->config.height;
|
|
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;
|
|
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);
|
|
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.position);
|
|
view->opponent_carried_key = opponent.carried_key;
|
|
}
|
|
} else {
|
|
view->self_cell_index = UINT16_MAX;
|
|
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_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];
|
|
}
|
|
}
|
|
|
|
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_passages(game, cell);
|
|
view->cells[view->cell_count].opened_passages = game->opened_passages[index];
|
|
if (x + 1U < game->maze->config.width) {
|
|
maze32_cell_t east = {x + 1U, y};
|
|
if (reveal_all || is_visible(center, east, radius)) {
|
|
view->cells[view->cell_count].trail_east = trail_owner_mask(
|
|
game, trail_edge_index(game->maze, cell, MAZE32_PASSAGE_EAST));
|
|
}
|
|
}
|
|
if (y + 1U < game->maze->config.height) {
|
|
maze32_cell_t south = {x, y + 1U};
|
|
if (reveal_all || is_visible(center, south, radius)) {
|
|
view->cells[view->cell_count].trail_south = trail_owner_mask(
|
|
game, trail_edge_index(game->maze, cell, MAZE32_PASSAGE_SOUTH));
|
|
}
|
|
}
|
|
view->cell_count++;
|
|
}
|
|
}
|
|
}
|
|
project_objects(game, view, center, radius, reveal_all);
|
|
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 = 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;
|
|
}
|
|
|
|
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) {
|
|
static const char* const NAMES[] = {"countdown", "active", "finish_pending", "paused",
|
|
"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,"
|
|
"\"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_connected[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->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);
|
|
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\",\"result\":\"%s\",\"width\":%u,\"height\":%u,"
|
|
"\"meetingSequence\":%lu,"
|
|
"\"chunk\":%u,\"chunks\":%u",
|
|
(unsigned long)view->state_sequence, role_name(view->role),
|
|
phase_name(view->phase), final_result_name(view->final_result), view->width,
|
|
view->height, (unsigned long)view->meeting_sequence, (unsigned int)chunk_index,
|
|
(unsigned int)total_chunks)) {
|
|
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 (!status_appended || !append_text(output, output_size, &offset, ",\"cells\":[")) {
|
|
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
|
|
}
|
|
|
|
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;
|
|
}
|
|
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,%u,%u,%u]",
|
|
index == first ? "" : ",", cell->cell_index, cell->passages,
|
|
cell->opened_passages, cell->trail_east, cell->trail_south)) {
|
|
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 (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)) {
|
|
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", "no_key", "player_exited", "match_finished",
|
|
};
|
|
if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_MATCH_FINISHED) {
|
|
return "unknown";
|
|
}
|
|
return NAMES[result];
|
|
}
|