feat(game): implement authoritative movement and visibility core

This commit is contained in:
2026-09-04 11:29:20 +03:00
parent 5d780def9d
commit 2aaa1650ae
11 changed files with 1085 additions and 7 deletions
+380
View File
@@ -0,0 +1,380 @@
#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_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 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;
}
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->players[0].position = maze->starts[0];
game->players[1].position = maze->starts[1];
game->next_command_sequence = 1U;
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)) {
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)) {
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->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_command_result_t maze32_game_process_next(maze32_game_t* game) {
maze32_game_command_result_t processed = {.result = MAZE32_GAME_RESULT_INVALID_ARGUMENT};
if (game == NULL) {
return processed;
}
processed.state_sequence = game->state_sequence;
processed.meeting_sequence = game->meeting_sequence;
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) {
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->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;
}
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;
}
return processed;
}
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) {
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);
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) {
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) {
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)) {
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->opponent_cell_index = UINT16_MAX;
int reveal_all = role == MAZE32_GAME_ROLE_OBSERVER;
maze32_cell_t center = {0U, 0U};
if (role_is_player(role)) {
uint8_t player_index = (uint8_t)role;
center = game->players[player_index].position;
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->opponent_visible = 1U;
view->opponent_cell_index = cell_index(game->maze, opponent);
}
} 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 (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->maze->passages[index];
view->cell_count++;
}
}
}
project_objects(game, view, center, radius, reveal_all);
return MAZE32_GAME_RESULT_ACCEPTED;
}
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 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) {
return phase == MAZE32_GAME_PHASE_ACTIVE ? "active" : "countdown";
}
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) {
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\",\"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)) {
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;
}
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\":[")) {
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;
if (chunk_index < cell_chunks) {
size_t first = chunk_index * MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
size_t end = first + MAZE32_GAME_VIEW_CELLS_PER_CHUNK;
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]", index == first ? "" : ",",
cell->cell_index, cell->passages)) {
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 (!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"};
if (result < MAZE32_GAME_RESULT_ACCEPTED || result > MAZE32_GAME_RESULT_BUFFER_TOO_SMALL) {
return "unknown";
}
return NAMES[result];
}