feat(game): complete authoritative gameplay mechanics
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user