feat: complete human-vs-human gameplay end to end
This commit is contained in:
+6
-2
@@ -1,7 +1,7 @@
|
||||
CC ?= cc
|
||||
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
|
||||
|
||||
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service
|
||||
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration
|
||||
|
||||
test_command_queue: test_command_queue.c ../../src/command_queue.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
@@ -14,6 +14,7 @@ run: all
|
||||
./test_state_presenter
|
||||
./test_http_api
|
||||
./test_sync_service
|
||||
./test_human_game_integration
|
||||
|
||||
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
@@ -33,5 +34,8 @@ test_http_api: test_http_api.c ../../src/http_api.c ../../src/application.c ../.
|
||||
test_sync_service: test_sync_service.c ../../src/sync_service.c ../../src/application.c ../../src/command_queue.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/fleet_generator.c ../../src/game_engine.c ../../src/state_presenter.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
|
||||
test_human_game_integration: test_human_game_integration.c ../../src/http_api.c ../../src/application.c ../../src/command_queue.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/fleet_generator.c ../../src/game_engine.c ../../src/state_presenter.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
|
||||
clean:
|
||||
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service
|
||||
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "http_api.h"
|
||||
|
||||
typedef struct { uint32_t value; } test_random_t;
|
||||
|
||||
static uint32_t next_random(void *context) {
|
||||
test_random_t *random = context;
|
||||
random->value = random->value * 1664525U + 1013904223U;
|
||||
return random->value;
|
||||
}
|
||||
|
||||
static http_api_response_t call(http_api_t *api, http_api_route_t route, http_api_method_t method,
|
||||
const char *body, const char *token) {
|
||||
http_api_response_t response;
|
||||
const http_api_request_t request = {.method = method, .route = route, .content_type_json = true,
|
||||
.body = body, .body_length = body == NULL ? 0U : strlen(body), .session_token = token};
|
||||
assert(http_api_handle(api, &request, &response));
|
||||
assert(response.body_length == strlen(response.body));
|
||||
return response;
|
||||
}
|
||||
|
||||
static void token_from_response(const http_api_response_t *response, char token[33]) {
|
||||
const char *start = strstr(response->body, "\"token\":\"");
|
||||
assert(start != NULL);
|
||||
memcpy(token, start + strlen("\"token\":\""), 32U);
|
||||
token[32] = '\0';
|
||||
}
|
||||
|
||||
static const char *board_start(const char *json, uint8_t board) {
|
||||
const char *first = strstr(json, "\"boards\":[\"");
|
||||
assert(first != NULL);
|
||||
first += strlen("\"boards\":[\"");
|
||||
return board == 0U ? first : first + kBoardCellCount + 3U;
|
||||
}
|
||||
|
||||
static void assert_hidden(const char *json, uint8_t board) {
|
||||
const char *cells = board_start(json, board);
|
||||
for (uint8_t index = 0; index < kBoardCellCount; ++index) assert(cells[index] != '1');
|
||||
}
|
||||
|
||||
static void resume(http_api_t *api, const char token[33], const char *role) {
|
||||
char body[48];
|
||||
snprintf(body, sizeof(body), "{\"token\":\"%s\"}", token);
|
||||
const http_api_response_t response = call(api, HTTP_API_ROUTE_RESUME, HTTP_API_POST, body, NULL);
|
||||
assert(response.status == 200U && strstr(response.body, role) != NULL);
|
||||
}
|
||||
|
||||
static void command(http_api_t *api, http_api_route_t route, const char token[33], uint32_t game_id) {
|
||||
char body[80];
|
||||
snprintf(body, sizeof(body), "{\"token\":\"%s\",\"gameId\":%u}", token, (unsigned int)game_id);
|
||||
const http_api_response_t response = call(api, route, HTTP_API_POST, body, NULL);
|
||||
assert(response.status == 200U);
|
||||
}
|
||||
|
||||
static void shot(http_api_t *api, const char token[33], uint32_t game_id, coordinate_t coordinate) {
|
||||
char body[96];
|
||||
snprintf(body, sizeof(body), "{\"token\":\"%s\",\"gameId\":%u,\"x\":%u,\"y\":%u}", token,
|
||||
(unsigned int)game_id, coordinate.x, coordinate.y);
|
||||
const http_api_response_t response = call(api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, body, NULL);
|
||||
assert(response.status == 200U);
|
||||
}
|
||||
|
||||
static coordinate_t first_cell(const board_t *board, cell_t cell) {
|
||||
for (uint8_t index = 0; index < kBoardCellCount; ++index) {
|
||||
if (board->cells[index] == cell) return (coordinate_t){.x = (uint8_t)(index % kBoardWidth), .y = (uint8_t)(index / kBoardWidth)};
|
||||
}
|
||||
assert(false);
|
||||
return (coordinate_t){0};
|
||||
}
|
||||
|
||||
static void test_human_game_journey(void) {
|
||||
test_random_t random = {.value = 57U};
|
||||
application_t application;
|
||||
application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random});
|
||||
http_api_t api;
|
||||
http_api_init(&api, &application);
|
||||
char player_tokens[2][33];
|
||||
|
||||
http_api_response_t response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST,
|
||||
"{\"name\":\"Алиса\",\"requestedRole\":\"player1\"}", NULL);
|
||||
assert(response.status == 200U);
|
||||
token_from_response(&response, player_tokens[0]);
|
||||
response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST,
|
||||
"{\"name\":\"Борис\",\"requestedRole\":\"player2\"}", NULL);
|
||||
assert(response.status == 200U);
|
||||
token_from_response(&response, player_tokens[1]);
|
||||
response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST,
|
||||
"{\"name\":\"Зритель\",\"requestedRole\":\"spectator\"}", NULL);
|
||||
assert(response.status == 200U);
|
||||
char spectator_token[33];
|
||||
token_from_response(&response, spectator_token);
|
||||
|
||||
resume(&api, player_tokens[0], "player1");
|
||||
resume(&api, player_tokens[1], "player2");
|
||||
resume(&api, spectator_token, "spectator");
|
||||
const uint32_t game_id = application.lifecycle.game.state.game_id;
|
||||
char config[96];
|
||||
snprintf(config, sizeof(config), "{\"token\":\"%s\",\"gameId\":%u,\"mode\":\"human\"}",
|
||||
player_tokens[0], (unsigned int)game_id);
|
||||
response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, config, NULL);
|
||||
assert(response.status == 200U);
|
||||
command(&api, HTTP_API_ROUTE_START, player_tokens[0], game_id);
|
||||
assert(application.lifecycle.game.state.phase == PHASE_IN_PROGRESS);
|
||||
|
||||
response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_tokens[0]);
|
||||
assert(response.status == 200U && board_start(response.body, 0)[0] <= '3');
|
||||
assert_hidden(response.body, 1U);
|
||||
response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_tokens[1]);
|
||||
assert(response.status == 200U);
|
||||
assert_hidden(response.body, 0U);
|
||||
response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, spectator_token);
|
||||
assert(response.status == 200U);
|
||||
assert_hidden(response.body, 0U);
|
||||
assert_hidden(response.body, 1U);
|
||||
|
||||
uint8_t first_player = application.lifecycle.game.state.current_player;
|
||||
shot(&api, player_tokens[first_player], game_id, first_cell(&application.lifecycle.game.state.boards[first_player ^ 1U], CELL_WATER));
|
||||
const uint8_t winner = application.lifecycle.game.state.current_player;
|
||||
assert(winner == (first_player ^ 1U));
|
||||
bool saw_sunk = false;
|
||||
while (application.lifecycle.game.state.phase == PHASE_IN_PROGRESS) {
|
||||
const coordinate_t target = first_cell(&application.lifecycle.game.state.boards[winner ^ 1U], CELL_SHIP);
|
||||
shot(&api, player_tokens[winner], game_id, target);
|
||||
response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, spectator_token);
|
||||
saw_sunk = saw_sunk || strchr(board_start(response.body, winner ^ 1U), '4') != NULL;
|
||||
}
|
||||
assert(saw_sunk);
|
||||
assert(application.lifecycle.game.state.winner == winner);
|
||||
assert(application.lifecycle.game.state.statistics[winner].shots == 20U);
|
||||
assert(application.lifecycle.game.state.statistics[winner].hits == 20U);
|
||||
assert(application.lifecycle.game.state.statistics[winner].ships_sunk == 10U);
|
||||
assert(application.lifecycle.game.state.statistics[first_player].misses == 1U);
|
||||
response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, spectator_token);
|
||||
assert(response.status == 200U && strstr(response.body, "\"winner\":") != NULL);
|
||||
assert(strstr(response.body, "\"statistics\":[") != NULL);
|
||||
assert(strchr(board_start(response.body, 0U), '1') != NULL || strchr(board_start(response.body, 1U), '1') != NULL);
|
||||
|
||||
resume(&api, player_tokens[0], "player1");
|
||||
resume(&api, player_tokens[1], "player2");
|
||||
command(&api, HTTP_API_ROUTE_REMATCH, player_tokens[0], game_id);
|
||||
assert(application.lifecycle.game.state.phase == PHASE_REMATCH_WAIT);
|
||||
resume(&api, spectator_token, "spectator");
|
||||
command(&api, HTTP_API_ROUTE_REMATCH, player_tokens[1], game_id);
|
||||
assert(application.lifecycle.game.state.phase == PHASE_IN_PROGRESS && application.lifecycle.game.state.game_id != game_id);
|
||||
|
||||
assert(game_lifecycle_disconnect(&application.lifecycle, 1U) == LIFECYCLE_RESULT_OK);
|
||||
command(&api, HTTP_API_ROUTE_ABORT, player_tokens[0], application.lifecycle.game.state.game_id);
|
||||
assert(application.lifecycle.game.state.phase == PHASE_LOBBY);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_human_game_journey();
|
||||
puts("human game integration tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -31,6 +31,7 @@ static void test_role_views_and_golden_schema(void) {
|
||||
state.phase = PHASE_IN_PROGRESS;
|
||||
state.mode = MODE_HUMAN;
|
||||
state.current_player = 1U;
|
||||
state.winner = kPlayerCapacity;
|
||||
set_board(&state.boards[0]);
|
||||
set_board(&state.boards[1]);
|
||||
char output[kStateMessageCapacity] = {0};
|
||||
@@ -50,6 +51,7 @@ static void test_role_views_and_golden_schema(void) {
|
||||
assert_hidden(output, 0);
|
||||
assert_hidden(output, 1);
|
||||
assert(board_start(output, 0)[1] == '2' && board_start(output, 1)[2] == '3');
|
||||
assert(strstr(output, "\"winner\":null,\"statistics\":[[0,0,0,0],[0,0,0,0]]") != NULL);
|
||||
}
|
||||
|
||||
static void test_finished_and_failure_are_safe(void) {
|
||||
@@ -71,10 +73,23 @@ static void test_finished_and_failure_are_safe(void) {
|
||||
lifecycle.game.state = state;
|
||||
lifecycle.game.state.phase = PHASE_REMATCH_WAIT;
|
||||
lifecycle.game.state.mode = MODE_HUMAN;
|
||||
lifecycle.game.state.winner = 1U;
|
||||
lifecycle.game.state.statistics[0] = (match_statistics_t){.shots = 9U, .hits = 4U, .misses = 5U, .ships_sunk = 2U};
|
||||
lifecycle.game.state.statistics[1] = (match_statistics_t){.shots = 8U, .hits = 5U, .misses = 3U, .ships_sunk = 3U};
|
||||
lifecycle.cumulative[0].wins = UINT16_MAX;
|
||||
lifecycle.cumulative[1].wins = UINT16_MAX;
|
||||
assert(state_presenter_write_lifecycle(&lifecycle, ROLE_SPECTATOR, output, sizeof(output), &written));
|
||||
assert(written == 371U);
|
||||
assert(written < sizeof(output));
|
||||
assert(strstr(output, "\"winner\":1,\"statistics\":[[9,4,5,2],[8,5,3,3]]") != NULL);
|
||||
|
||||
lifecycle.game.state.statistics[0] = (match_statistics_t){UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT8_MAX};
|
||||
lifecycle.game.state.statistics[1] = (match_statistics_t){UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT8_MAX};
|
||||
assert(state_presenter_write_lifecycle(&lifecycle, ROLE_SPECTATOR, output, sizeof(output), &written));
|
||||
assert(written < sizeof(output));
|
||||
|
||||
state.boards[0].ships[0] = (ship_t){.x = 2U, .y = 0U, .length = 1U, .hits = 1U, .horizontal = true};
|
||||
assert(state_presenter_write(&state, ROLE_SPECTATOR, output, sizeof(output), &written));
|
||||
assert(board_start(output, 0)[2] == '4');
|
||||
|
||||
char unchanged[16];
|
||||
memset(unchanged, 'X', sizeof(unchanged));
|
||||
|
||||
Reference in New Issue
Block a user