feat(game): add persistent bush-wall breakthroughs

This commit is contained in:
2026-09-10 23:12:42 +03:00
parent bb5bd73ebd
commit e10ad3acb8
12 changed files with 354 additions and 39 deletions
@@ -10,6 +10,8 @@ extern "C" {
#endif
#define MAZE32_GAME_PLAYER_COUNT 2U
#define MAZE32_GAME_BREAKTHROUGH_HITS 5U
#define MAZE32_GAME_BREAKTHROUGH_TIMEOUT_MS 3000U
#define MAZE32_GAME_COMMAND_CAPACITY 16U
#define MAZE32_GAME_KEY_COUNT 2U
#define MAZE32_GAME_NO_KEY UINT8_MAX
@@ -101,6 +103,10 @@ typedef struct {
uint8_t exited;
uint8_t connected;
uint64_t reconnect_deadline_ms;
maze32_cell_t perseverance_cell;
maze32_game_direction_t perseverance_direction;
uint64_t perseverance_last_impact_ms;
uint8_t perseverance_hits;
} maze32_game_player_t;
typedef enum {
@@ -139,6 +145,8 @@ typedef struct {
uint8_t activated_artifact;
uint8_t opened_exit;
uint8_t outward_exit;
uint8_t breakthrough_hits;
uint8_t breakthrough_opened;
uint8_t finished;
uint8_t key;
uint8_t artifact;
@@ -154,6 +162,7 @@ typedef struct {
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];
uint8_t opened_passages[MAZE32_MAZE_MAX_CELLS];
maze32_game_command_t commands[MAZE32_GAME_COMMAND_CAPACITY];
uint8_t command_head;
uint8_t command_count;
@@ -179,6 +188,7 @@ typedef enum {
typedef struct {
uint16_t cell_index;
uint8_t passages;
uint8_t opened_passages;
} maze32_view_cell_t;
typedef struct {
+85 -4
View File
@@ -20,6 +20,19 @@ static maze32_passage_t direction_passage(maze32_game_direction_t direction) {
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--;
@@ -45,6 +58,43 @@ 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 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;
@@ -95,6 +145,9 @@ 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) {
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;
@@ -159,6 +212,7 @@ int maze32_game_advance_time(maze32_game_t* game, uint64_t now_ms) {
game->paused_started_ms = 0U;
game->command_head = 0U;
game->command_count = 0U;
reset_all_perseverance(game);
game->state_sequence++;
return 1;
}
@@ -176,6 +230,7 @@ int maze32_game_advance_time(maze32_game_t* game, uint64_t now_ms) {
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;
}
@@ -219,6 +274,7 @@ maze32_game_result_t maze32_game_set_player_connected(maze32_game_t* game, uint8
}
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;
@@ -229,6 +285,7 @@ maze32_game_result_t maze32_game_set_player_connected(maze32_game_t* game, uint8
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;
@@ -373,6 +430,7 @@ static void finish_outward_move(maze32_game_t* game, uint8_t player_index, uint6
} else {
game->phase = MAZE32_GAME_PHASE_FINISHED;
game->final_result = MAZE32_GAME_FINAL_DRAW;
reset_all_perseverance(game);
processed->finished = 1U;
}
}
@@ -452,6 +510,7 @@ maze32_game_command_result_t maze32_game_process_next(maze32_game_t* game) {
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;
@@ -460,7 +519,27 @@ maze32_game_command_result_t maze32_game_process_next(maze32_game_t* game) {
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)) {
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;
}
@@ -589,7 +668,8 @@ maze32_game_result_t maze32_game_project_view(const maze32_game_t* game, maze32_
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->cells[view->cell_count].passages = game_passages(game, cell);
view->cells[view->cell_count].opened_passages = game->opened_passages[index];
view->cell_count++;
}
}
@@ -781,8 +861,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_cell_t* cell = &view->cells[index];
if (!append_text(output, output_size, &offset, "%s[%u,%u]", index == first ? "" : ",",
cell->cell_index, cell->passages)) {
if (!append_text(output, output_size, &offset, "%s[%u,%u,%u]",
index == first ? "" : ",", cell->cell_index, cell->passages,
cell->opened_passages)) {
return MAZE32_GAME_RESULT_BUFFER_TOO_SMALL;
}
}
+20 -1
View File
@@ -722,6 +722,20 @@ static void send_game_error(maze32_gateway_t* gateway, int socket_fd, const char
send_error(gateway, socket_fd, request_id, maze32_game_result_name(result), message);
}
static void send_breakthrough_impact(maze32_gateway_t* gateway, int socket_fd,
const char* request_id, uint8_t hits) {
char response[MAZE32_GATEWAY_OUTGOING_LIMIT] = "";
int written = snprintf(response, sizeof(response),
"{\"v\":1,\"type\":\"error\",\"seq\":%" PRIu32
",\"requestId\":\"%s\",\"payload\":{\"code\":\"blocked\","
"\"message\":\"Там стена. Очень убедительная\","
"\"breakthroughHits\":%u}}",
next_server_sequence(gateway), request_id, (unsigned int)hits);
if (written >= 0 && (size_t)written < sizeof(response)) {
enqueue(gateway, socket_fd, response);
}
}
static void handle_game_command(maze32_gateway_t* gateway, const maze32_session_t* session,
const maze32_client_message_t* message) {
if (session->role == MAZE32_ROLE_OBSERVER) {
@@ -753,7 +767,12 @@ static void handle_game_command(maze32_gateway_t* gateway, const maze32_session_
if (finished_now) {
complete_match(gateway);
}
send_game_error(gateway, session->connection_id, message->request_id, result);
if (result == MAZE32_GAME_RESULT_BLOCKED && processed.breakthrough_hits > 0U) {
send_breakthrough_impact(gateway, session->connection_id, message->request_id,
processed.breakthrough_hits);
} else {
send_game_error(gateway, session->connection_id, message->request_id, result);
}
return;
}
char acknowledgement[160] = "";