feat(game): complete authoritative gameplay mechanics

This commit is contained in:
2026-09-04 12:25:37 +03:00
parent 115a3bd0a1
commit 9339578dc9
13 changed files with 8795 additions and 109 deletions
+21 -8
View File
@@ -223,7 +223,7 @@ static void test_visibility_and_serialization(void) {
game.players[0].position = (maze32_cell_t){3U, 3U};
game.players[1].position = (maze32_cell_t){4U, 4U};
maze.passages[index_of(3U, 3U)] = 0U;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U, &view) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 49U);
assert(view_has_cell(&view, index_of(6U, 6U)));
@@ -231,7 +231,7 @@ static void test_visibility_and_serialization(void) {
game.players[0].position = (maze32_cell_t){0U, 0U};
game.players[1].position = (maze32_cell_t){7U, 7U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U, &view) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 16U);
assert(!view.opponent_visible && view.opponent_cell_index == UINT16_MAX);
@@ -239,7 +239,7 @@ static void test_visibility_and_serialization(void) {
assert(!view_has_object(&view, MAZE32_VIEW_OBJECT_KEY, index_of(7U, 7U)));
serialize_and_inspect(&view, 1);
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_TWO, 3U, &view) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_TWO, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 16U && view.self_cell_index == index_of(7U, 7U));
assert(!view.opponent_visible && !view_has_cell(&view, index_of(0U, 0U)));
@@ -256,18 +256,19 @@ static void test_visibility_and_serialization(void) {
assert(strstr(player_two_payload, "[\"key\",0,0,10]") == NULL);
}
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U, &view) ==
game.players[0].vision_moves_remaining = 1U;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 49U);
assert(!view_has_cell(&view, index_of(7U, 7U)));
game.players[0].position = (maze32_cell_t){3U, 3U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U, &view) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 64U);
assert(view.opponent_visible);
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, 0U, &view) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 64U);
assert(view.object_count == 10U);
@@ -286,12 +287,24 @@ static void test_default_map_chunk_bounds(void) {
assert(maze32_maze_generate(&maze, &workspace, &maze_config, UINT32_C(0x5a320006), &report) ==
MAZE32_MAZE_OK);
maze32_game_t game = initialized_game(&maze);
game.phase = MAZE32_GAME_PHASE_FINISHED;
game.final_result = MAZE32_GAME_FINAL_DRAW;
game.state_sequence = UINT32_MAX;
game.meeting_sequence = UINT32_MAX;
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
game.players[player].vision_moves_remaining = MAZE32_GAME_VISION_MOVES;
game.players[player].compass_hint = MAZE32_GAME_COMPASS_NORTH_WEST;
game.players[player].exited = 1U;
game.exits[player].open = 1U;
game.has_exit_time[player] = 1U;
game.exit_times_ms[player] = UINT64_MAX - player;
}
maze32_view_t view;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, 0U, &view) ==
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.cell_count == 8192U && view.object_count == 30U);
size_t chunks = maze32_game_view_chunk_count(&view);
assert(chunks == 345U);
assert(chunks == 688U);
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
size_t largest_chunk = 0U;
for (size_t chunk = 0U; chunk < chunks; ++chunk) {
+358
View File
@@ -0,0 +1,358 @@
#include "maze32_game.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define TEST_WIDTH 8U
#define TEST_HEIGHT 8U
static uint16_t index_of(uint16_t x, uint16_t y) {
return (uint16_t)(y * TEST_WIDTH + x);
}
static void open_test_grid(maze32_maze_t* maze) {
memset(maze, 0, sizeof(*maze));
maze->config.width = TEST_WIDTH;
maze->config.height = TEST_HEIGHT;
maze->config.artifact_pairs = 1U;
for (uint16_t y = 0U; y < TEST_HEIGHT; ++y) {
for (uint16_t x = 0U; x < TEST_WIDTH; ++x) {
uint8_t passages = 0U;
if (y > 0U) {
passages |= MAZE32_PASSAGE_NORTH;
}
if (x + 1U < TEST_WIDTH) {
passages |= MAZE32_PASSAGE_EAST;
}
if (y + 1U < TEST_HEIGHT) {
passages |= MAZE32_PASSAGE_SOUTH;
}
if (x > 0U) {
passages |= MAZE32_PASSAGE_WEST;
}
maze->passages[index_of(x, y)] = passages;
}
}
maze->starts[0] = (maze32_cell_t){0U, 1U};
maze->starts[1] = (maze32_cell_t){7U, 6U};
maze->keys[0] = (maze32_cell_t){2U, 1U};
maze->keys[1] = (maze32_cell_t){1U, 1U};
maze->exits[0] = (maze32_cell_t){0U, 1U};
maze->exits[1] = (maze32_cell_t){7U, 6U};
maze->compasses[0] = (maze32_cell_t){1U, 1U};
maze->compasses[1] = (maze32_cell_t){6U, 6U};
maze->artifacts[0][0] = (maze32_cell_t){1U, 2U};
maze->artifacts[0][1] = (maze32_cell_t){7U, 7U};
}
static maze32_game_t active_game(maze32_maze_t* maze) {
maze32_game_t game;
maze32_game_config_t config = maze32_game_default_config();
config.movement_cooldown_ms = 0U;
assert(maze32_game_init(&game, maze, &config) == MAZE32_GAME_RESULT_ACCEPTED);
maze32_game_set_phase(&game, MAZE32_GAME_PHASE_ACTIVE);
return game;
}
static maze32_game_command_result_t move_player(maze32_game_t* game, maze32_game_role_t role,
maze32_game_direction_t direction,
uint64_t submitted_ms) {
uint32_t sequence = 0U;
assert(maze32_game_enqueue_move(game, role, direction, submitted_ms, &sequence) ==
MAZE32_GAME_RESULT_ACCEPTED);
maze32_game_command_result_t result = maze32_game_process_next(game);
assert(result.command_sequence == sequence);
return result;
}
static maze32_game_command_result_t drop_key(maze32_game_t* game, maze32_game_role_t role,
uint64_t submitted_ms) {
uint32_t sequence = 0U;
assert(maze32_game_enqueue_drop_key(game, role, submitted_ms, &sequence) ==
MAZE32_GAME_RESULT_ACCEPTED);
maze32_game_command_result_t result = maze32_game_process_next(game);
assert(result.command_sequence == sequence);
return result;
}
static void give_key(maze32_game_t* game, uint8_t carrier, uint8_t key) {
game->players[carrier].carried_key = key;
game->keys[key].location = MAZE32_GAME_KEY_CARRIED;
game->keys[key].carrier = carrier;
}
static void assert_key_conservation(const maze32_game_t* game) {
uint8_t references[MAZE32_GAME_KEY_COUNT] = {0U, 0U};
for (uint8_t key = 0U; key < MAZE32_GAME_KEY_COUNT; ++key) {
if (game->keys[key].location == MAZE32_GAME_KEY_GROUND) {
references[key]++;
assert(game->keys[key].carrier == UINT8_MAX);
} else {
assert(game->keys[key].carrier < MAZE32_GAME_PLAYER_COUNT);
}
}
for (uint8_t player = 0U; player < MAZE32_GAME_PLAYER_COUNT; ++player) {
uint8_t key = game->players[player].carried_key;
if (key != MAZE32_GAME_NO_KEY) {
assert(key < MAZE32_GAME_KEY_COUNT);
assert(game->keys[key].location == MAZE32_GAME_KEY_CARRIED);
assert(game->keys[key].carrier == player);
references[key]++;
}
}
assert(references[0] == 1U && references[1] == 1U);
}
static void test_inventory_drop_and_sabotage(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 1U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.picked_key);
assert(result.key == 1U && game.players[0].carried_key == 1U);
assert_key_conservation(&game);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 2U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.picked_key);
assert(game.players[0].carried_key == 1U);
assert(game.keys[0].location == MAZE32_GAME_KEY_GROUND);
assert_key_conservation(&game);
result = drop_key(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 3U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.dropped_key && result.key == 1U);
assert(game.players[0].carried_key == MAZE32_GAME_NO_KEY);
assert(game.keys[0].location == MAZE32_GAME_KEY_GROUND &&
game.keys[1].location == MAZE32_GAME_KEY_GROUND);
assert(game.keys[0].ground_position.x == 2U && game.keys[1].ground_position.x == 2U);
assert_key_conservation(&game);
assert(move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 4U).result ==
MAZE32_GAME_RESULT_ACCEPTED);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 5U);
assert(result.picked_key && result.key == 0U);
assert(game.players[0].carried_key == 0U);
assert(game.keys[1].location == MAZE32_GAME_KEY_GROUND);
assert_key_conservation(&game);
uint32_t state_sequence = game.state_sequence;
game.players[0].carried_key = MAZE32_GAME_NO_KEY;
game.keys[0].location = MAZE32_GAME_KEY_GROUND;
game.keys[0].ground_position = game.players[0].position;
game.keys[0].carrier = UINT8_MAX;
result = drop_key(&game, MAZE32_GAME_ROLE_PLAYER_ONE, 6U);
assert(result.result == MAZE32_GAME_RESULT_NO_KEY && game.state_sequence == state_sequence);
assert_key_conservation(&game);
printf("game key inventory, drop, and sabotage: PASS\n");
}
static void test_atomic_meeting_return(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
game.players[0].position = (maze32_cell_t){2U, 2U};
game.players[1].position = (maze32_cell_t){4U, 2U};
give_key(&game, 0U, 1U);
give_key(&game, 1U, 0U);
assert_key_conservation(&game);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_WEST, 10U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.meeting);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 10U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.meeting);
assert(result.returned_keys == 3U);
assert(game.players[0].carried_key == 0U && game.players[1].carried_key == 1U);
assert(game.keys[0].carrier == 0U && game.keys[1].carrier == 1U);
assert_key_conservation(&game);
printf("game atomic meeting key return: PASS (opponent keys swapped to owners)\n");
}
static void test_owner_exit_restrictions(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
game.players[0].position = (maze32_cell_t){1U, 1U};
give_key(&game, 0U, 1U);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 20U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.opened_exit);
assert(!game.exits[0].open);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 21U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED && game.phase == MAZE32_GAME_PHASE_ACTIVE);
open_test_grid(&maze);
game = active_game(&maze);
game.players[0].position = (maze32_cell_t){6U, 6U};
give_key(&game, 0U, 1U);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 30U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.opened_exit);
assert(!game.exits[1].open);
game.exits[1].open = 1U;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 31U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED && !game.players[0].exited);
open_test_grid(&maze);
game = active_game(&maze);
game.players[0].position = (maze32_cell_t){1U, 1U};
give_key(&game, 0U, 0U);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 40U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.opened_exit);
assert(game.exits[0].open && game.phase == MAZE32_GAME_PHASE_ACTIVE);
assert(game.players[0].carried_key == 0U);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 41U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.outward_exit);
assert(game.phase == MAZE32_GAME_PHASE_FINISH_PENDING &&
game.final_result == MAZE32_GAME_FINAL_NONE);
printf("game owner-only exit opening and outward move: PASS\n");
}
static void test_vision_artifact_duration(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze.starts[0] = (maze32_cell_t){0U, 2U};
maze32_game_t game = active_game(&maze);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 100U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && result.activated_artifact);
assert(result.artifact == 0U && game.consumed_artifacts[0]);
assert(game.players[0].vision_moves_remaining == MAZE32_GAME_VISION_MOVES);
assert(maze32_game_player_view_radius(&game, 0U) == 6U);
maze.passages[index_of(1U, 2U)] &= (uint8_t)~MAZE32_PASSAGE_NORTH;
uint32_t accepted_moves = game.players[0].accepted_moves;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_NORTH, 101U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED);
assert(game.players[0].vision_moves_remaining == 10U);
assert(game.players[0].accepted_moves == accepted_moves);
for (uint8_t move = 0U; move < MAZE32_GAME_VISION_MOVES; ++move) {
maze32_game_direction_t direction =
move % 2U == 0U ? MAZE32_GAME_DIRECTION_WEST : MAZE32_GAME_DIRECTION_EAST;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, direction, 102U + move);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED && !result.activated_artifact);
assert(game.players[0].vision_moves_remaining == MAZE32_GAME_VISION_MOVES - move - 1U);
if (move < MAZE32_GAME_VISION_MOVES - 1U) {
assert(maze32_game_player_view_radius(&game, 0U) == 6U);
}
}
assert(maze32_game_player_view_radius(&game, 0U) == 3U);
printf("game one-use vision duration: PASS (10 successful moves, failed move ignored)\n");
}
static void test_compass_and_role_projections(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze.exits[0] = (maze32_cell_t){7U, 0U};
maze32_game_t game = active_game(&maze);
give_key(&game, 0U, 1U);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_EAST, 200U);
assert(result.compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
assert(game.players[0].compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
maze32_view_t view;
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.self_carried_key == 1U && view.self_compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
char payload[MAZE32_GAME_VIEW_CHUNK_BYTES];
size_t written = 0U;
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"hint\":\"ne\"") != NULL);
assert(strstr(payload, "\"inventory\":1") != NULL);
assert(strstr(payload, "\"exitTimes\"") == NULL);
game.keys[0].location = MAZE32_GAME_KEY_CARRIED;
game.keys[0].carrier = 1U;
game.players[1].carried_key = 0U;
game.players[1].position = (maze32_cell_t){2U, 1U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_ONE, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.opponent_visible && view.opponent_carried_key == 0U);
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"opponentKey\":0") != NULL);
game.players[1].position = (maze32_cell_t){7U, 7U};
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_PLAYER_TWO, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(!view.opponent_visible);
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"hint\":\"ne\"") == NULL);
assert(strstr(payload, "\"opponentKey\"") == NULL);
assert(maze32_game_project_view(&game, MAZE32_GAME_ROLE_OBSERVER, &view) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(view.observer_carried_keys[0] == 1U);
assert(maze32_game_serialize_view_chunk(&view, 0U, payload, sizeof(payload), &written) ==
MAZE32_GAME_RESULT_ACCEPTED);
assert(strstr(payload, "\"players\":[") != NULL);
assert(strstr(payload, "\"key\":1") != NULL);
assert(strstr(payload, "\"exitTimes\":[null,null]") != NULL);
maze.passages[index_of(1U, 1U)] &= (uint8_t)~MAZE32_PASSAGE_NORTH;
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_NORTH, 201U);
assert(result.result == MAZE32_GAME_RESULT_BLOCKED);
assert(game.players[0].compass_hint == MAZE32_GAME_COMPASS_NORTH_EAST);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 202U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
assert(game.players[0].compass_hint == MAZE32_GAME_COMPASS_NONE);
printf("game compass bearing and role-specific projections: PASS\n");
}
static void prepare_open_exits(maze32_game_t* game) {
game->players[0].position = game->maze->exits[0];
game->players[1].position = game->maze->exits[1];
game->exits[0].open = 1U;
game->exits[1].open = 1U;
}
static void test_victory_and_draw_window(void) {
maze32_maze_t maze;
open_test_grid(&maze);
maze32_game_t game = active_game(&maze);
prepare_open_exits(&game);
maze32_game_command_result_t result =
move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 1000U);
assert(result.outward_exit && !result.finished);
assert(game.phase == MAZE32_GAME_PHASE_FINISH_PENDING);
assert(!maze32_game_advance_time(&game, 1200U));
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_EAST, 1200U);
assert(result.outward_exit && result.finished);
assert(game.phase == MAZE32_GAME_PHASE_FINISHED && game.final_result == MAZE32_GAME_FINAL_DRAW);
open_test_grid(&maze);
game = active_game(&maze);
prepare_open_exits(&game);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_ONE, MAZE32_GAME_DIRECTION_WEST, 2000U);
assert(result.result == MAZE32_GAME_RESULT_ACCEPTED);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_EAST, 2201U);
assert(result.result == MAZE32_GAME_RESULT_MATCH_FINISHED && result.finished);
assert(!game.players[1].exited && game.final_result == MAZE32_GAME_FINAL_PLAYER_ONE);
open_test_grid(&maze);
game = active_game(&maze);
prepare_open_exits(&game);
result = move_player(&game, MAZE32_GAME_ROLE_PLAYER_TWO, MAZE32_GAME_DIRECTION_EAST, 3000U);
assert(result.outward_exit && game.final_result == MAZE32_GAME_FINAL_NONE);
assert(!maze32_game_advance_time(&game, 3200U));
assert(maze32_game_advance_time(&game, 3201U));
assert(game.final_result == MAZE32_GAME_FINAL_PLAYER_TWO);
printf("game outward result window: PASS (200 ms draw boundary, 201 ms winner)\n");
}
int main(void) {
test_inventory_drop_and_sabotage();
test_atomic_meeting_return();
test_owner_exit_restrictions();
test_vision_artifact_duration();
test_compass_and_role_projections();
test_victory_and_draw_window();
puts("game mechanics tests: PASS");
return 0;
}