feat: complete human-vs-human gameplay end to end
This commit is contained in:
@@ -738,7 +738,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record,
|
|||||||
|
|
||||||
## Milestone 014 — Complete human-vs-human gameplay end to end
|
## Milestone 014 — Complete human-vs-human gameplay end to end
|
||||||
|
|
||||||
**Status:** `READY`
|
**Status:** `DONE`
|
||||||
**Depends on:** Milestone 013
|
**Depends on:** Milestone 013
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
@@ -765,11 +765,22 @@ Integrate and prove the complete two-device human-vs-human journey before enabli
|
|||||||
|
|
||||||
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 015 from `BLOCKED` to `READY`.
|
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 015 from `BLOCKED` to `READY`.
|
||||||
|
|
||||||
|
### Execution record
|
||||||
|
|
||||||
|
- Date: 2026-08-29
|
||||||
|
- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2.
|
||||||
|
- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; `esp_littlefs` 1.20.4.
|
||||||
|
- Result: PASS for automated integration verification; pending physical two-device confirmation.
|
||||||
|
- Evidence: The role-safe state contract now identifies sunk hits, exposes the authoritative winner after completion, and carries compact per-player match statistics. The Russian result screen renders these values. Added a deterministic host integration journey through the actual HTTP command path: two player joins, spectator join, role-safe board filtering, player and spectator token resume before and during the game, a complete generated human-versus-human game, turn handoff after a miss, all ten ships sunk, finish/revealed boards/statistics, dual rematch confirmation with a new game ID, and disconnected-player abort. Existing state serialization coverage also exercises maximum statistics values within the fixed message buffer.
|
||||||
|
- Measurements: `make -C test/host run` passed command queue, domain, bot, lifecycle, state-presenter, HTTP API, synchronization, and the new human-game integration suites. `node --check data/app.js`, gzip integrity checks, and compressed JavaScript syntax checks passed. Compressed/source web assets total 31,909 B. `pio run -e esp32-c6-devkitm-1 -t buildfs` and `pio run -e esp32-c6-devkitm-1` passed; firmware uses 39,348 / 327,680 B RAM (12.0%) and 1,015,002 / 2,097,152 B flash (48.4%).
|
||||||
|
- Issues or deviations: No firmware upload or physical client testing was performed. The two-phone full-game, real Wi-Fi/WebSocket reconnect, and browser-rendered spectator checks remain required device validation steps.
|
||||||
|
- Next action: Milestone 015 is ready. Do not start it unless explicitly requested.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 015 — Complete human-vs-ESP32 gameplay and cumulative statistics
|
## Milestone 015 — Complete human-vs-ESP32 gameplay and cumulative statistics
|
||||||
|
|
||||||
**Status:** `BLOCKED`
|
**Status:** `READY`
|
||||||
**Depends on:** Milestone 014
|
**Depends on:** Milestone 014
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|||||||
+9
-3
@@ -84,8 +84,11 @@
|
|||||||
|
|
||||||
function safeState(payload) {
|
function safeState(payload) {
|
||||||
return payload && payload.type === 'state' && Array.isArray(payload.boards) && payload.boards.length === 2 &&
|
return payload && payload.type === 'state' && Array.isArray(payload.boards) && payload.boards.length === 2 &&
|
||||||
payload.boards.every(board => typeof board === 'string' && /^[0123]{100}$/.test(board)) &&
|
payload.boards.every(board => typeof board === 'string' && /^[01234]{100}$/.test(board)) &&
|
||||||
typeof payload.version === 'number' && typeof payload.gameId === 'number';
|
typeof payload.version === 'number' && typeof payload.gameId === 'number' &&
|
||||||
|
(payload.winner === null || payload.winner === 0 || payload.winner === 1) && Array.isArray(payload.statistics) &&
|
||||||
|
payload.statistics.length === 2 && payload.statistics.every(entry => Array.isArray(entry) && entry.length === 4 &&
|
||||||
|
entry.every(value => Number.isInteger(value) && value >= 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
function acceptState(payload) {
|
function acceptState(payload) {
|
||||||
@@ -109,6 +112,7 @@
|
|||||||
if (value === '1') return ['ship', 'Корабль'];
|
if (value === '1') return ['ship', 'Корабль'];
|
||||||
if (value === '2') return ['miss', 'Промах'];
|
if (value === '2') return ['miss', 'Промах'];
|
||||||
if (value === '3') return ['hit', 'Попадание'];
|
if (value === '3') return ['hit', 'Попадание'];
|
||||||
|
if (value === '4') return ['sunk', 'Потопленный корабль'];
|
||||||
return ['', 'Вода'];
|
return ['', 'Вода'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +212,9 @@
|
|||||||
function renderResult() {
|
function renderResult() {
|
||||||
showScreen('result');
|
showScreen('result');
|
||||||
const waiting = state.phase === 'rematch_wait';
|
const waiting = state.phase === 'rematch_wait';
|
||||||
ui.result.textContent = waiting ? 'Ожидается подтверждение повторной игры.' : 'Поля раскрыты. Можно подтвердить повторную игру.';
|
const winner = state.winner === 0 || state.winner === 1 ? `Победил игрок ${state.winner + 1}. ` : '';
|
||||||
|
const statistics = state.statistics.map((entry, index) => `Игрок ${index + 1}: выстрелы ${entry[0]}, попадания ${entry[1]}, промахи ${entry[2]}, потоплено ${entry[3]}.`).join(' ');
|
||||||
|
ui.result.textContent = `${winner}${statistics} ${waiting ? 'Ожидается подтверждение повторной игры.' : 'Поля раскрыты. Можно подтвердить повторную игру.'}`;
|
||||||
ui.rematch.hidden = !isPlayer();
|
ui.rematch.hidden = !isPlayer();
|
||||||
ui.rematch.disabled = !isPlayer();
|
ui.rematch.disabled = !isPlayer();
|
||||||
ui.rematch.textContent = waiting ? 'Подтвердить повторно' : 'Сыграть ещё';
|
ui.rematch.textContent = waiting ? 'Подтвердить повторно' : 'Сыграть ещё';
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ All numeric fields are decimal JSON integers, never strings.
|
|||||||
| `gameId`, `version` | unsigned 32-bit integer |
|
| `gameId`, `version` | unsigned 32-bit integer |
|
||||||
| `x`, `y` | integer 0–9 |
|
| `x`, `y` | integer 0–9 |
|
||||||
| `name` | 1–20 scalar values, at most 80 UTF-8 bytes |
|
| `name` | 1–20 scalar values, at most 80 UTF-8 bytes |
|
||||||
| `board` | exactly 100 ASCII cells: `0` unknown/water, `1` own ship, `2` miss, `3` hit |
|
| `board` | exactly 100 ASCII cells: `0` unknown/water, `1` revealed ship, `2` miss, `3` hit, `4` sunk hit |
|
||||||
|
|
||||||
Every state-changing request has `token` and `gameId`; they must precede any
|
Every state-changing request has `token` and `gameId`; they must precede any
|
||||||
mutation validation. A stale `gameId` is rejected as `STALE_GAME`.
|
mutation validation. A stale `gameId` is rejected as `STALE_GAME`.
|
||||||
@@ -66,12 +66,14 @@ The HTTP state response and WebSocket `state` event use this single 512-byte
|
|||||||
maximum schema:
|
maximum schema:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"type":"state","version":17,"gameId":4,"phase":"in_progress","mode":"human","viewer":"player1","turn":"player2","boards":["000...100 cells...","000...100 cells..."],"wins":[0,0]}
|
{"type":"state","version":17,"gameId":4,"phase":"in_progress","mode":"human","viewer":"player1","turn":"player2","boards":["000...100 cells...","000...100 cells..."],"wins":[0,0],"winner":null,"statistics":[[3,2,1,1],[4,1,3,0]]}
|
||||||
```
|
```
|
||||||
|
|
||||||
`boards[0]` belongs to player 1 and `boards[1]` to player 2. The presenter
|
`boards[0]` belongs to player 1 and `boards[1]` to player 2. The presenter
|
||||||
replaces every unauthorized unhit ship with `0`. In `finished`, both boards may
|
replaces every unauthorized unhit ship with `0`. In `finished`, both boards may
|
||||||
contain `1`. No other event contains a board.
|
contain `1`. `winner` is `null` until `finished`, then player index `0` or `1`.
|
||||||
|
Each compact statistics tuple is `[shots,hits,misses,shipsSunk]`. No other event
|
||||||
|
contains a board.
|
||||||
|
|
||||||
## WebSocket
|
## WebSocket
|
||||||
|
|
||||||
|
|||||||
+30
-2
@@ -45,13 +45,36 @@ static char present_cell(cell_t cell, bool reveal_ships) {
|
|||||||
return cell == CELL_SHIP && reveal_ships ? '1' : '0';
|
return cell == CELL_SHIP && reveal_ships ? '1' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool cell_is_sunk(const board_t *board, uint8_t cell_index) {
|
||||||
|
const uint8_t x = (uint8_t)(cell_index % kBoardWidth);
|
||||||
|
const uint8_t y = (uint8_t)(cell_index / kBoardWidth);
|
||||||
|
for (uint8_t index = 0; index < kFleetShipCount; ++index) {
|
||||||
|
const ship_t *ship = &board->ships[index];
|
||||||
|
for (uint8_t offset = 0; offset < ship->length; ++offset) {
|
||||||
|
const uint8_t ship_x = ship->horizontal ? (uint8_t)(ship->x + offset) : ship->x;
|
||||||
|
const uint8_t ship_y = ship->horizontal ? ship->y : (uint8_t)(ship->y + offset);
|
||||||
|
if (ship_x == x && ship_y == y) return ship->hits == ship->length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static bool append_board(state_writer_t *writer, const board_t *board, bool reveal_ships) {
|
static bool append_board(state_writer_t *writer, const board_t *board, bool reveal_ships) {
|
||||||
for (uint8_t index = 0; index < kBoardCellCount; ++index) {
|
for (uint8_t index = 0; index < kBoardCellCount; ++index) {
|
||||||
if (!append_character(writer, present_cell(board->cells[index], reveal_ships))) return false;
|
const char cell = board->cells[index] == CELL_HIT && cell_is_sunk(board, index) ? '4' :
|
||||||
|
present_cell(board->cells[index], reveal_ships);
|
||||||
|
if (!append_character(writer, cell)) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool append_statistics(state_writer_t *writer, const match_statistics_t *statistics) {
|
||||||
|
return append_character(writer, '[') && append_u32(writer, statistics->shots) &&
|
||||||
|
append_character(writer, ',') && append_u32(writer, statistics->hits) &&
|
||||||
|
append_character(writer, ',') && append_u32(writer, statistics->misses) &&
|
||||||
|
append_character(writer, ',') && append_u32(writer, statistics->ships_sunk) && append_character(writer, ']');
|
||||||
|
}
|
||||||
|
|
||||||
static bool write_state(const game_state_t *state, role_t viewer, const cumulative_statistics_t *cumulative,
|
static bool write_state(const game_state_t *state, role_t viewer, const cumulative_statistics_t *cumulative,
|
||||||
char *output, size_t output_size, size_t *written) {
|
char *output, size_t output_size, size_t *written) {
|
||||||
if (written != NULL) *written = 0;
|
if (written != NULL) *written = 0;
|
||||||
@@ -71,7 +94,12 @@ static bool write_state(const game_state_t *state, role_t viewer, const cumulati
|
|||||||
!append_text(&writer, "\",\"boards\":[\"") || !append_board(&writer, &state->boards[0], reveal[0]) ||
|
!append_text(&writer, "\",\"boards\":[\"") || !append_board(&writer, &state->boards[0], reveal[0]) ||
|
||||||
!append_text(&writer, "\",\"") || !append_board(&writer, &state->boards[1], reveal[1]) ||
|
!append_text(&writer, "\",\"") || !append_board(&writer, &state->boards[1], reveal[1]) ||
|
||||||
!append_text(&writer, "\"],\"wins\":[") || !append_u32(&writer, wins_0) ||
|
!append_text(&writer, "\"],\"wins\":[") || !append_u32(&writer, wins_0) ||
|
||||||
!append_character(&writer, ',') || !append_u32(&writer, wins_1) || !append_text(&writer, "]}")) return false;
|
!append_character(&writer, ',') || !append_u32(&writer, wins_1) ||
|
||||||
|
!append_text(&writer, "],\"winner\":") ||
|
||||||
|
!(state->winner < kPlayerCapacity ? append_u32(&writer, state->winner) : append_text(&writer, "null")) ||
|
||||||
|
!append_text(&writer, ",\"statistics\":[") || !append_statistics(&writer, &state->statistics[0]) ||
|
||||||
|
!append_character(&writer, ',') || !append_statistics(&writer, &state->statistics[1]) ||
|
||||||
|
!append_text(&writer, "]}")) return false;
|
||||||
if (writer.length + 1U > output_size) return false;
|
if (writer.length + 1U > output_size) return false;
|
||||||
writer.data[writer.length] = '\0';
|
writer.data[writer.length] = '\0';
|
||||||
memcpy(output, writer.data, writer.length + 1U);
|
memcpy(output, writer.data, writer.length + 1U);
|
||||||
|
|||||||
+6
-2
@@ -1,7 +1,7 @@
|
|||||||
CC ?= cc
|
CC ?= cc
|
||||||
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
|
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
|
test_command_queue: test_command_queue.c ../../src/command_queue.c
|
||||||
$(CC) $(CFLAGS) $^ -o $@
|
$(CC) $(CFLAGS) $^ -o $@
|
||||||
@@ -14,6 +14,7 @@ run: all
|
|||||||
./test_state_presenter
|
./test_state_presenter
|
||||||
./test_http_api
|
./test_http_api
|
||||||
./test_sync_service
|
./test_sync_service
|
||||||
|
./test_human_game_integration
|
||||||
|
|
||||||
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
|
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
|
||||||
$(CC) $(CFLAGS) $^ -o $@
|
$(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
|
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 $@
|
$(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:
|
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.phase = PHASE_IN_PROGRESS;
|
||||||
state.mode = MODE_HUMAN;
|
state.mode = MODE_HUMAN;
|
||||||
state.current_player = 1U;
|
state.current_player = 1U;
|
||||||
|
state.winner = kPlayerCapacity;
|
||||||
set_board(&state.boards[0]);
|
set_board(&state.boards[0]);
|
||||||
set_board(&state.boards[1]);
|
set_board(&state.boards[1]);
|
||||||
char output[kStateMessageCapacity] = {0};
|
char output[kStateMessageCapacity] = {0};
|
||||||
@@ -50,6 +51,7 @@ static void test_role_views_and_golden_schema(void) {
|
|||||||
assert_hidden(output, 0);
|
assert_hidden(output, 0);
|
||||||
assert_hidden(output, 1);
|
assert_hidden(output, 1);
|
||||||
assert(board_start(output, 0)[1] == '2' && board_start(output, 1)[2] == '3');
|
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) {
|
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 = state;
|
||||||
lifecycle.game.state.phase = PHASE_REMATCH_WAIT;
|
lifecycle.game.state.phase = PHASE_REMATCH_WAIT;
|
||||||
lifecycle.game.state.mode = MODE_HUMAN;
|
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[0].wins = UINT16_MAX;
|
||||||
lifecycle.cumulative[1].wins = UINT16_MAX;
|
lifecycle.cumulative[1].wins = UINT16_MAX;
|
||||||
assert(state_presenter_write_lifecycle(&lifecycle, ROLE_SPECTATOR, output, sizeof(output), &written));
|
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];
|
char unchanged[16];
|
||||||
memset(unchanged, 'X', sizeof(unchanged));
|
memset(unchanged, 'X', sizeof(unchanged));
|
||||||
|
|||||||
Reference in New Issue
Block a user