feat: complete human-vs-ESP32 gameplay and cumulative statistics

This commit is contained in:
2026-08-29 23:33:27 +03:00
parent 650ade2726
commit c8e0c9168b
14 changed files with 269 additions and 19 deletions
+10 -6
View File
@@ -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 test_human_game_integration
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_bot_game_integration
test_command_queue: test_command_queue.c ../../src/command_queue.c
$(CC) $(CFLAGS) $^ -o $@
@@ -15,6 +15,7 @@ run: all
./test_http_api
./test_sync_service
./test_human_game_integration
./test_bot_game_integration
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
@@ -22,20 +23,23 @@ test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_
test_bot_player: test_bot_player.c ../../src/bot_player.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
test_game_lifecycle: test_game_lifecycle.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/fleet_generator.c ../../src/game_engine.c
test_game_lifecycle: test_game_lifecycle.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/bot_player.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
test_state_presenter: test_state_presenter.c ../../src/state_presenter.c
$(CC) $(CFLAGS) $^ -o $@
test_http_api: test_http_api.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
test_http_api: test_http_api.c ../../src/http_api.c ../../src/application.c ../../src/command_queue.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/bot_player.c ../../src/fleet_generator.c ../../src/game_engine.c ../../src/state_presenter.c
$(CC) $(CFLAGS) $^ -o $@
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/bot_player.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
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/bot_player.c ../../src/fleet_generator.c ../../src/game_engine.c ../../src/state_presenter.c
$(CC) $(CFLAGS) $^ -o $@
test_bot_game_integration: test_bot_game_integration.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/bot_player.c ../../src/fleet_generator.c ../../src/game_engine.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 test_human_game_integration
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 test_bot_game_integration
+107
View File
@@ -0,0 +1,107 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "game_lifecycle.h"
typedef struct { uint32_t value; } test_random_t;
typedef struct { uint32_t delay_ms; uint8_t calls; } test_scheduler_t;
static uint32_t next_random(void *context) {
test_random_t *random = context;
random->value = random->value * 1664525U + 1013904223U;
return random->value;
}
static bool schedule_after(void *context, uint32_t delay_ms) {
test_scheduler_t *scheduler = context;
scheduler->delay_ms = delay_ms;
++scheduler->calls;
return true;
}
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_bot_game_lifecycle(void) {
test_random_t random = {.value = 101U};
test_scheduler_t scheduler = {0};
game_lifecycle_t lifecycle;
game_lifecycle_init(&lifecycle, (random_source_t){.next_u32 = next_random, .context = &random});
game_lifecycle_set_bot_scheduler(&lifecycle, (scheduler_t){.schedule_after_ms = schedule_after, .context = &scheduler});
uint8_t player = 0;
assert(game_lifecycle_join(&lifecycle, ROLE_PLAYER_1, "Alice", &player) == LIFECYCLE_RESULT_OK);
const uint32_t game_id = lifecycle.game.state.game_id;
assert(game_lifecycle_configure(&lifecycle, player, game_id, MODE_BOT) == LIFECYCLE_RESULT_OK);
assert(game_lifecycle_start(&lifecycle, player, game_id) == LIFECYCLE_RESULT_OK);
assert(lifecycle.game.state.phase == PHASE_IN_PROGRESS && lifecycle.game.state.mode == MODE_BOT);
if (lifecycle.game.state.current_player == 0U) {
assert(game_lifecycle_shot(&lifecycle, player, game_id, first_cell(&lifecycle.game.state.boards[1], CELL_WATER), NULL) == LIFECYCLE_RESULT_OK);
}
assert(lifecycle.game.state.current_player == 1U);
assert(lifecycle.bot.turn_pending && scheduler.calls == 1U);
assert(scheduler.delay_ms >= 500U && scheduler.delay_ms <= 900U);
assert(game_lifecycle_bot_take_turn(&lifecycle));
assert(lifecycle.game.state.statistics[1].shots == 1U);
assert(lifecycle.game.state.statistics[1].shots == lifecycle.game.state.statistics[1].hits + lifecycle.game.state.statistics[1].misses);
for (uint16_t attempts = 0; attempts < kBoardCellCount && lifecycle.game.state.current_player == 1U &&
lifecycle.game.state.phase == PHASE_IN_PROGRESS; ++attempts) {
assert(game_lifecycle_bot_take_turn(&lifecycle));
}
assert(lifecycle.game.state.phase == PHASE_IN_PROGRESS && lifecycle.game.state.current_player == 0U);
while (lifecycle.game.state.phase == PHASE_IN_PROGRESS) {
assert(game_lifecycle_shot(&lifecycle, player, game_id, first_cell(&lifecycle.game.state.boards[1], CELL_SHIP), NULL) == LIFECYCLE_RESULT_OK);
}
assert(lifecycle.game.state.winner == 0U);
assert(lifecycle.game.state.statistics[0].ships_sunk == kFleetShipCount);
for (uint8_t side = 0; side < kPlayerCapacity; ++side) {
const match_statistics_t *match = &lifecycle.game.state.statistics[side];
const cumulative_statistics_t *total = &lifecycle.cumulative[side];
assert(total->games == 1U && total->shots == match->shots && total->hits == match->hits &&
total->misses == match->misses && total->ships_sunk == match->ships_sunk);
}
assert(lifecycle.cumulative[0].wins == 1U && lifecycle.cumulative[1].losses == 1U);
assert(game_lifecycle_rematch(&lifecycle, player, game_id) == LIFECYCLE_RESULT_OK);
assert(lifecycle.game.state.phase == PHASE_IN_PROGRESS && lifecycle.game.state.game_id != game_id);
assert(lifecycle.game.state.statistics[0].shots == 0U && lifecycle.game.state.statistics[1].shots == 0U);
assert(lifecycle.cumulative[0].games == 1U && lifecycle.cumulative[0].wins == 1U);
game_lifecycle_init(&lifecycle, (random_source_t){.next_u32 = next_random, .context = &random});
assert(lifecycle.game.state.phase == PHASE_LOBBY && lifecycle.game.state.game_id == 1U);
assert(lifecycle.cumulative[0].games == 0U && lifecycle.cumulative[1].games == 0U);
assert(!lifecycle.sessions.entries[0].occupied && !lifecycle.sessions.entries[1].occupied);
}
static void test_bot_can_receive_the_first_turn(void) {
for (uint32_t seed = 1U; seed < 100U; ++seed) {
test_random_t random = {.value = seed};
test_scheduler_t scheduler = {0};
game_lifecycle_t lifecycle;
game_lifecycle_init(&lifecycle, (random_source_t){.next_u32 = next_random, .context = &random});
game_lifecycle_set_bot_scheduler(&lifecycle, (scheduler_t){.schedule_after_ms = schedule_after, .context = &scheduler});
uint8_t player = 0;
assert(game_lifecycle_join(&lifecycle, ROLE_PLAYER_1, "Alice", &player) == LIFECYCLE_RESULT_OK);
assert(game_lifecycle_configure(&lifecycle, player, lifecycle.game.state.game_id, MODE_BOT) == LIFECYCLE_RESULT_OK);
assert(game_lifecycle_start(&lifecycle, player, lifecycle.game.state.game_id) == LIFECYCLE_RESULT_OK);
if (lifecycle.game.state.current_player != 1U) continue;
assert(lifecycle.bot.turn_pending && scheduler.calls == 1U);
assert(scheduler.delay_ms >= 500U && scheduler.delay_ms <= 900U);
return;
}
assert(false);
}
int main(void) {
test_bot_game_lifecycle();
test_bot_can_receive_the_first_turn();
puts("bot game integration tests passed");
return 0;
}
+5
View File
@@ -140,6 +140,11 @@ static void test_sessions_commands_and_state(void) {
assert(response.status == 200U && strstr(response.body, "\"viewer\":\"player1\"") != NULL);
response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, "bad");
expect_code(&response, 401U, "UNAUTHORIZED");
response = call(&api, HTTP_API_ROUTE_STATISTICS, HTTP_API_GET, NULL, player_1_token);
assert(response.status == 200U && strstr(response.body, "\"viewer\":\"player1\"") != NULL &&
strstr(response.body, "\"match\":[[") != NULL && strstr(response.body, "\"cumulative\":[[") != NULL);
response = call(&api, HTTP_API_ROUTE_STATISTICS, HTTP_API_GET, NULL, "bad");
expect_code(&response, 401U, "UNAUTHORIZED");
snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"x\":10,\"y\":0}", player_1_token);
response = call(&api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, command, NULL);