diff --git a/PLANS.md b/PLANS.md index dfca0f3..a395d05 100644 --- a/PLANS.md +++ b/PLANS.md @@ -436,7 +436,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record, ## Milestone 007 — Implement and exhaustively test the game domain core -**Status:** `READY` +**Status:** `DONE` **Depends on:** Milestone 006 ### Objective @@ -465,11 +465,22 @@ Implement all deterministic Battleship rules independently of Wi-Fi, HTTP, WebSo If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 008 from `BLOCKED` to `READY`. +### Execution record + +- Date: 2026-08-28 +- 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. +- Evidence: Added a transport-independent fixed-size fleet generator and game engine. Fleet generation has 64 restarts with 128 bounded placement attempts per ship and a final boundary, composition, and no-touch validator. The game engine enforces phase, turn, coordinate, and repeated-shot validation; hit retention, miss handoff, sunk-ship border misses, victory, per-match statistics, and exactly one version increment for each accepted start or shot. +- Measurements: `make -C test/host run` passed 10,000 deterministic fleet generations, 10,000 fully simulated games, and rule/error atomicity tests. `pio run -e esp32-c6-devkitm-1` passed with 38,212 / 327,680 B RAM (11.7%) and 1,000,730 / 2,097,152 B flash (47.7%), within the Milestone 007 gates of 190,000 B remaining heap and 1,180,000 B firmware. +- Issues or deviations: No network-facing command integration or bot behavior was added; these remain later milestones. +- Next action: Milestone 008 is ready. Do not start it unless explicitly requested. + --- ## Milestone 008 — Implement the ESP32 opponent -**Status:** `BLOCKED` +**Status:** `READY` **Depends on:** Milestone 007 ### Objective diff --git a/include/fleet_generator.h b/include/fleet_generator.h index ce47392..e6c364c 100644 --- a/include/fleet_generator.h +++ b/include/fleet_generator.h @@ -3,4 +3,12 @@ #include "app_interfaces.h" #include "game_types.h" +enum { + kFleetPlacementAttempts = 128, + kFleetGenerationRestarts = 64, +}; + typedef struct { random_source_t random; } fleet_generator_t; + +bool fleet_generator_generate(const fleet_generator_t *generator, board_t *board); +bool fleet_generator_validate(const board_t *board); diff --git a/include/game_engine.h b/include/game_engine.h index e9a58ef..3cff7e4 100644 --- a/include/game_engine.h +++ b/include/game_engine.h @@ -1,6 +1,27 @@ #pragma once +#include + +#include "app_interfaces.h" +#include "fleet_generator.h" #include "game_types.h" -/* Rule implementation begins in Milestone 007. */ typedef struct { game_state_t state; } game_engine_t; + +typedef uint8_t game_result_t; +enum { + GAME_RESULT_OK, + GAME_RESULT_INVALID_COORDINATE, + GAME_RESULT_WRONG_PHASE, + GAME_RESULT_NOT_YOUR_TURN, + GAME_RESULT_CELL_ALREADY_SHOT, + GAME_RESULT_GENERATION_FAILED, +}; + +typedef struct { bool hit; bool sunk; bool finished; } shot_result_t; + +void game_engine_init(game_engine_t *engine); +game_result_t game_engine_start(game_engine_t *engine, uint32_t game_id, game_mode_t mode, + const fleet_generator_t *generator); +game_result_t game_engine_shot(game_engine_t *engine, uint8_t player, coordinate_t coordinate, + shot_result_t *result); diff --git a/include/game_types.h b/include/game_types.h index 0637a3c..ee5e1e1 100644 --- a/include/game_types.h +++ b/include/game_types.h @@ -17,7 +17,17 @@ enum { MODE_HUMAN, MODE_BOT }; typedef struct { uint8_t x; uint8_t y; } coordinate_t; typedef struct { uint8_t x; uint8_t y; uint8_t length; uint8_t hits; bool horizontal; } ship_t; typedef struct { cell_t cells[kBoardCellCount]; ship_t ships[kFleetShipCount]; uint8_t ships_alive; } board_t; -typedef struct { uint32_t game_id; uint32_t version; phase_t phase; game_mode_t mode; uint8_t current_player; board_t boards[kPlayerCapacity]; } game_state_t; +typedef struct { uint16_t shots; uint16_t hits; uint16_t misses; uint8_t ships_sunk; } match_statistics_t; +typedef struct { + uint32_t game_id; + uint32_t version; + phase_t phase; + game_mode_t mode; + uint8_t current_player; + uint8_t winner; + board_t boards[kPlayerCapacity]; + match_statistics_t statistics[kPlayerCapacity]; +} game_state_t; _Static_assert(kBoardCellCount == 100, "board contract changed"); _Static_assert(kFleetShipCount == 10, "fleet contract changed"); @@ -25,3 +35,5 @@ _Static_assert(kSessionCapacity == 10, "session contract changed"); _Static_assert(sizeof(coordinate_t) == 2, "coordinate must remain compact"); _Static_assert(sizeof(ship_t) <= 6, "ship grew beyond fixed budget"); _Static_assert(sizeof(board_t) <= 164, "board grew beyond fixed budget"); +_Static_assert(sizeof(match_statistics_t) <= 8, "statistics grew beyond fixed budget"); +_Static_assert(sizeof(game_state_t) <= 360, "game state grew beyond fixed budget"); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ba7218a..74d9797 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,7 +2,7 @@ # without default 'CMakeLists.txt' file. idf_component_register( - SRCS "main.c" "application.c" "command_queue.c" + SRCS "main.c" "application.c" "command_queue.c" "fleet_generator.c" "game_engine.c" INCLUDE_DIRS "../include" REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash ) diff --git a/src/fleet_generator.c b/src/fleet_generator.c new file mode 100644 index 0000000..742df8d --- /dev/null +++ b/src/fleet_generator.c @@ -0,0 +1,119 @@ +#include "fleet_generator.h" + +#include +#include + +static const uint8_t kFleetLengths[kFleetShipCount] = {4, 3, 3, 2, 2, 2, 1, 1, 1, 1}; + +static bool coordinate_is_valid(uint8_t x, uint8_t y) { + return x < kBoardWidth && y < kBoardHeight; +} + +static uint8_t cell_index(uint8_t x, uint8_t y) { + return (uint8_t)(y * kBoardWidth + x); +} + +static bool random_u32(const fleet_generator_t *generator, uint32_t *value) { + if (generator == NULL || generator->random.next_u32 == NULL || value == NULL) return false; + *value = generator->random.next_u32(generator->random.context); + return true; +} + +static bool placement_fits(const board_t *board, uint8_t x, uint8_t y, uint8_t length, + bool horizontal) { + const uint8_t end_x = horizontal ? (uint8_t)(x + length - 1U) : x; + const uint8_t end_y = horizontal ? y : (uint8_t)(y + length - 1U); + if (!coordinate_is_valid(end_x, end_y)) return false; + + const uint8_t min_x = x == 0 ? 0 : (uint8_t)(x - 1U); + const uint8_t min_y = y == 0 ? 0 : (uint8_t)(y - 1U); + const uint8_t max_x = end_x + 1U >= kBoardWidth ? (uint8_t)(kBoardWidth - 1U) : (uint8_t)(end_x + 1U); + const uint8_t max_y = end_y + 1U >= kBoardHeight ? (uint8_t)(kBoardHeight - 1U) : (uint8_t)(end_y + 1U); + for (uint8_t check_y = min_y; check_y <= max_y; ++check_y) { + for (uint8_t check_x = min_x; check_x <= max_x; ++check_x) { + if (board->cells[cell_index(check_x, check_y)] == CELL_SHIP) return false; + } + } + return true; +} + +static void place_ship(board_t *board, uint8_t ship_index, uint8_t x, uint8_t y, uint8_t length, + bool horizontal) { + ship_t *ship = &board->ships[ship_index]; + *ship = (ship_t){.x = x, .y = y, .length = length, .hits = 0, .horizontal = horizontal}; + for (uint8_t offset = 0; offset < length; ++offset) { + const uint8_t cell_x = horizontal ? (uint8_t)(x + offset) : x; + const uint8_t cell_y = horizontal ? y : (uint8_t)(y + offset); + board->cells[cell_index(cell_x, cell_y)] = CELL_SHIP; + } +} + +bool fleet_generator_generate(const fleet_generator_t *generator, board_t *board) { + if (board == NULL) return false; + for (uint8_t restart = 0; restart < kFleetGenerationRestarts; ++restart) { + board_t candidate = {0}; + memset(candidate.cells, CELL_WATER, sizeof(candidate.cells)); + bool complete = true; + for (uint8_t ship_index = 0; ship_index < kFleetShipCount; ++ship_index) { + bool placed = false; + for (uint16_t attempt = 0; attempt < kFleetPlacementAttempts; ++attempt) { + uint32_t value = 0; + if (!random_u32(generator, &value)) return false; + const bool horizontal = (value & 1U) != 0U; + const uint8_t x = (uint8_t)((value >> 1U) % kBoardWidth); + const uint8_t y = (uint8_t)((value >> 5U) % kBoardHeight); + const uint8_t length = kFleetLengths[ship_index]; + if (!placement_fits(&candidate, x, y, length, horizontal)) continue; + place_ship(&candidate, ship_index, x, y, length, horizontal); + placed = true; + break; + } + if (!placed) { + complete = false; + break; + } + } + candidate.ships_alive = complete ? kFleetShipCount : 0U; + if (complete && fleet_generator_validate(&candidate)) { + *board = candidate; + return true; + } + } + return false; +} + +bool fleet_generator_validate(const board_t *board) { + if (board == NULL || board->ships_alive != kFleetShipCount) return false; + uint8_t expected_cells[kBoardCellCount] = {0}; + for (uint8_t ship_index = 0; ship_index < kFleetShipCount; ++ship_index) { + const ship_t *ship = &board->ships[ship_index]; + if (ship->length != kFleetLengths[ship_index] || ship->hits > ship->length) return false; + const uint8_t end_x = ship->horizontal ? (uint8_t)(ship->x + ship->length - 1U) : ship->x; + const uint8_t end_y = ship->horizontal ? ship->y : (uint8_t)(ship->y + ship->length - 1U); + if (!coordinate_is_valid(ship->x, ship->y) || !coordinate_is_valid(end_x, end_y)) return false; + for (uint8_t offset = 0; offset < ship->length; ++offset) { + const uint8_t x = ship->horizontal ? (uint8_t)(ship->x + offset) : ship->x; + const uint8_t y = ship->horizontal ? ship->y : (uint8_t)(ship->y + offset); + const uint8_t index = cell_index(x, y); + if (expected_cells[index] != 0U || board->cells[index] != CELL_SHIP) return false; + expected_cells[index] = (uint8_t)(ship_index + 1U); + } + } + for (uint8_t y = 0; y < kBoardHeight; ++y) { + for (uint8_t x = 0; x < kBoardWidth; ++x) { + const uint8_t index = cell_index(x, y); + if ((board->cells[index] == CELL_SHIP) != (expected_cells[index] != 0U)) return false; + if (board->cells[index] != CELL_SHIP) continue; + for (int8_t delta_y = -1; delta_y <= 1; ++delta_y) { + for (int8_t delta_x = -1; delta_x <= 1; ++delta_x) { + const int16_t neighbour_x = (int16_t)x + delta_x; + const int16_t neighbour_y = (int16_t)y + delta_y; + if (neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= kBoardWidth || neighbour_y >= kBoardHeight) continue; + const uint8_t neighbour = cell_index((uint8_t)neighbour_x, (uint8_t)neighbour_y); + if (board->cells[neighbour] == CELL_SHIP && expected_cells[index] != expected_cells[neighbour]) return false; + } + } + } + } + return true; +} diff --git a/src/game_engine.c b/src/game_engine.c new file mode 100644 index 0000000..0c67c37 --- /dev/null +++ b/src/game_engine.c @@ -0,0 +1,111 @@ +#include "game_engine.h" + +#include +#include + +static uint8_t cell_index(coordinate_t coordinate) { + return (uint8_t)(coordinate.y * kBoardWidth + coordinate.x); +} + +static bool coordinate_is_valid(coordinate_t coordinate) { + return coordinate.x < kBoardWidth && coordinate.y < kBoardHeight; +} + +static ship_t *ship_at(board_t *board, coordinate_t coordinate) { + for (uint8_t index = 0; index < kFleetShipCount; ++index) { + ship_t *ship = &board->ships[index]; + for (uint8_t offset = 0; offset < ship->length; ++offset) { + const uint8_t x = ship->horizontal ? (uint8_t)(ship->x + offset) : ship->x; + const uint8_t y = ship->horizontal ? ship->y : (uint8_t)(ship->y + offset); + if (x == coordinate.x && y == coordinate.y) return ship; + } + } + return NULL; +} + +static void mark_sunk_ship_border(board_t *board, const ship_t *ship) { + const uint8_t end_x = ship->horizontal ? (uint8_t)(ship->x + ship->length - 1U) : ship->x; + const uint8_t end_y = ship->horizontal ? ship->y : (uint8_t)(ship->y + ship->length - 1U); + const uint8_t min_x = ship->x == 0 ? 0 : (uint8_t)(ship->x - 1U); + const uint8_t min_y = ship->y == 0 ? 0 : (uint8_t)(ship->y - 1U); + const uint8_t max_x = end_x + 1U >= kBoardWidth ? (uint8_t)(kBoardWidth - 1U) : (uint8_t)(end_x + 1U); + const uint8_t max_y = end_y + 1U >= kBoardHeight ? (uint8_t)(kBoardHeight - 1U) : (uint8_t)(end_y + 1U); + for (uint8_t y = min_y; y <= max_y; ++y) { + for (uint8_t x = min_x; x <= max_x; ++x) { + const uint8_t index = (uint8_t)(y * kBoardWidth + x); + if (board->cells[index] == CELL_WATER) board->cells[index] = CELL_MISS; + } + } +} + +void game_engine_init(game_engine_t *engine) { + if (engine == NULL) return; + memset(engine, 0, sizeof(*engine)); + engine->state.phase = PHASE_LOBBY; + engine->state.winner = kPlayerCapacity; +} + +game_result_t game_engine_start(game_engine_t *engine, uint32_t game_id, game_mode_t mode, + const fleet_generator_t *generator) { + if (engine == NULL || generator == NULL) return GAME_RESULT_GENERATION_FAILED; + if (engine->state.phase != PHASE_LOBBY) return GAME_RESULT_WRONG_PHASE; + if (mode != MODE_HUMAN && mode != MODE_BOT) return GAME_RESULT_WRONG_PHASE; + + board_t generated[kPlayerCapacity] = {0}; + for (uint8_t player = 0; player < kPlayerCapacity; ++player) { + if (!fleet_generator_generate(generator, &generated[player])) return GAME_RESULT_GENERATION_FAILED; + } + if (generator->random.next_u32 == NULL) return GAME_RESULT_GENERATION_FAILED; + const uint8_t first_player = (uint8_t)(generator->random.next_u32(generator->random.context) % kPlayerCapacity); + const uint32_t previous_version = engine->state.version; + memset(&engine->state, 0, sizeof(engine->state)); + engine->state.game_id = game_id; + engine->state.version = previous_version + 1U; + engine->state.phase = PHASE_IN_PROGRESS; + engine->state.mode = mode; + engine->state.current_player = first_player; + engine->state.winner = kPlayerCapacity; + memcpy(engine->state.boards, generated, sizeof(generated)); + return GAME_RESULT_OK; +} + +game_result_t game_engine_shot(game_engine_t *engine, uint8_t player, coordinate_t coordinate, + shot_result_t *result) { + if (engine == NULL || !coordinate_is_valid(coordinate)) return GAME_RESULT_INVALID_COORDINATE; + if (engine->state.phase != PHASE_IN_PROGRESS) return GAME_RESULT_WRONG_PHASE; + if (player >= kPlayerCapacity || player != engine->state.current_player) return GAME_RESULT_NOT_YOUR_TURN; + board_t *target = &engine->state.boards[player ^ 1U]; + cell_t *cell = &target->cells[cell_index(coordinate)]; + if (*cell == CELL_MISS || *cell == CELL_HIT) return GAME_RESULT_CELL_ALREADY_SHOT; + + shot_result_t accepted = {0}; + match_statistics_t *statistics = &engine->state.statistics[player]; + ++statistics->shots; + if (*cell == CELL_SHIP) { + *cell = CELL_HIT; + ++statistics->hits; + accepted.hit = true; + ship_t *ship = ship_at(target, coordinate); + if (ship != NULL) { + ++ship->hits; + if (ship->hits == ship->length) { + --target->ships_alive; + ++statistics->ships_sunk; + accepted.sunk = true; + mark_sunk_ship_border(target, ship); + } + } + if (target->ships_alive == 0U) { + engine->state.phase = PHASE_FINISHED; + engine->state.winner = player; + accepted.finished = true; + } + } else { + *cell = CELL_MISS; + ++statistics->misses; + engine->state.current_player ^= 1U; + } + ++engine->state.version; + if (result != NULL) *result = accepted; + return GAME_RESULT_OK; +} diff --git a/test/host/Makefile b/test/host/Makefile index 6e18a46..0c37393 100644 --- a/test/host/Makefile +++ b/test/host/Makefile @@ -1,13 +1,17 @@ CC ?= cc CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include -all: test_command_queue +all: test_command_queue test_game_domain test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/application.c $(CC) $(CFLAGS) $^ -o $@ -run: test_command_queue +run: all ./test_command_queue + ./test_game_domain + +test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c + $(CC) $(CFLAGS) $^ -o $@ clean: - rm -f test_command_queue + rm -f test_command_queue test_game_domain diff --git a/test/host/test_game_domain.c b/test/host/test_game_domain.c new file mode 100644 index 0000000..221fd18 --- /dev/null +++ b/test/host/test_game_domain.c @@ -0,0 +1,162 @@ +#include +#include +#include + +#include "fleet_generator.h" +#include "game_engine.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 fleet_generator_t generator_for(test_random_t *random) { + return (fleet_generator_t){.random = {.next_u32 = next_random, .context = random}}; +} + +static void start_game(game_engine_t *engine, test_random_t *random) { + const fleet_generator_t generator = generator_for(random); + game_engine_init(engine); + assert(game_engine_start(engine, 1, MODE_HUMAN, &generator) == GAME_RESULT_OK); + assert(engine->state.phase == PHASE_IN_PROGRESS); + assert(engine->state.version == 1U); + assert(fleet_generator_validate(&engine->state.boards[0])); + assert(fleet_generator_validate(&engine->state.boards[1])); +} + +static coordinate_t first_cell(const board_t *board, cell_t wanted) { + for (uint8_t y = 0; y < kBoardHeight; ++y) { + for (uint8_t x = 0; x < kBoardWidth; ++x) { + if (board->cells[y * kBoardWidth + x] == wanted) return (coordinate_t){x, y}; + } + } + assert(false); + return (coordinate_t){0, 0}; +} + +static void assert_rejected_unchanged(game_engine_t *engine, uint8_t player, coordinate_t coordinate, + game_result_t expected) { + game_engine_t before = *engine; + assert(game_engine_shot(engine, player, coordinate, NULL) == expected); + assert(memcmp(&before, engine, sizeof(before)) == 0); +} + +static void test_many_fleets(void) { + for (uint32_t seed = 0; seed < 10000U; ++seed) { + test_random_t random = {.value = seed}; + const fleet_generator_t generator = generator_for(&random); + board_t board = {0}; + assert(fleet_generator_generate(&generator, &board)); + assert(fleet_generator_validate(&board)); + } +} + +static void test_rules_and_rejection(void) { + test_random_t random = {.value = 7}; + game_engine_t engine; + start_game(&engine, &random); + const uint8_t player = engine.state.current_player; + game_engine_t before_start = engine; + const fleet_generator_t generator = generator_for(&random); + assert(game_engine_start(&engine, 2, MODE_HUMAN, &generator) == GAME_RESULT_WRONG_PHASE); + assert(memcmp(&before_start, &engine, sizeof(engine)) == 0); + assert_rejected_unchanged(&engine, player, (coordinate_t){10, 0}, GAME_RESULT_INVALID_COORDINATE); + assert_rejected_unchanged(&engine, player ^ 1U, (coordinate_t){0, 0}, GAME_RESULT_NOT_YOUR_TURN); + + const coordinate_t water = first_cell(&engine.state.boards[player ^ 1U], CELL_WATER); + shot_result_t result = {0}; + const uint32_t version = engine.state.version; + assert(game_engine_shot(&engine, player, water, &result) == GAME_RESULT_OK); + assert(!result.hit && !result.sunk && !result.finished); + assert(engine.state.current_player == (uint8_t)(player ^ 1U)); + assert(engine.state.version == version + 1U); + assert(engine.state.statistics[player].shots == 1U && engine.state.statistics[player].misses == 1U); + assert_rejected_unchanged(&engine, player, water, GAME_RESULT_NOT_YOUR_TURN); + + const uint8_t hitter = engine.state.current_player; + const coordinate_t ship_cell = first_cell(&engine.state.boards[hitter ^ 1U], CELL_SHIP); + assert(game_engine_shot(&engine, hitter, ship_cell, &result) == GAME_RESULT_OK); + assert(result.hit && !result.finished); + assert(engine.state.current_player == hitter); + assert(engine.state.statistics[hitter].hits == 1U); + assert_rejected_unchanged(&engine, hitter, ship_cell, GAME_RESULT_CELL_ALREADY_SHOT); +} + +static void test_sinking_and_victory(void) { + test_random_t random = {.value = 11}; + game_engine_t engine; + start_game(&engine, &random); + const uint8_t player = engine.state.current_player; + board_t *target = &engine.state.boards[player ^ 1U]; + const ship_t ship = target->ships[9]; + assert(ship.length == 1U); + const coordinate_t coordinate = {ship.x, ship.y}; + shot_result_t result = {0}; + assert(game_engine_shot(&engine, player, coordinate, &result) == GAME_RESULT_OK); + assert(result.hit && result.sunk && !result.finished); + assert(target->ships_alive == kFleetShipCount - 1U); + assert(engine.state.statistics[player].ships_sunk == 1U); + for (int8_t y = -1; y <= 1; ++y) { + for (int8_t x = -1; x <= 1; ++x) { + const int16_t neighbour_x = (int16_t)coordinate.x + x; + const int16_t neighbour_y = (int16_t)coordinate.y + y; + if (neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= kBoardWidth || neighbour_y >= kBoardHeight) continue; + const uint8_t index = (uint8_t)(neighbour_y * kBoardWidth + neighbour_x); + if (x != 0 || y != 0) assert(target->cells[index] == CELL_MISS); + } + } + + for (uint8_t ship_index = 0; ship_index < kFleetShipCount; ++ship_index) { + const ship_t remaining = target->ships[ship_index]; + for (uint8_t offset = 0; offset < remaining.length; ++offset) { + const coordinate_t hit = {remaining.horizontal ? (uint8_t)(remaining.x + offset) : remaining.x, + remaining.horizontal ? remaining.y : (uint8_t)(remaining.y + offset)}; + if (target->cells[hit.y * kBoardWidth + hit.x] == CELL_SHIP) assert(game_engine_shot(&engine, player, hit, &result) == GAME_RESULT_OK); + } + } + assert(result.finished); + assert(engine.state.phase == PHASE_FINISHED && engine.state.winner == player); + assert_rejected_unchanged(&engine, player, (coordinate_t){0, 0}, GAME_RESULT_WRONG_PHASE); +} + +static void test_simulated_games(void) { + for (uint32_t seed = 100; seed < 10100U; ++seed) { + test_random_t random = {.value = seed}; + game_engine_t engine; + start_game(&engine, &random); + for (uint16_t step = 0; step < 200U && engine.state.phase == PHASE_IN_PROGRESS; ++step) { + const uint8_t player = engine.state.current_player; + const board_t *target = &engine.state.boards[player ^ 1U]; + coordinate_t choice = {0, 0}; + bool found = false; + for (uint8_t index = 0; index < kBoardCellCount; ++index) { + if (target->cells[index] == CELL_WATER || target->cells[index] == CELL_SHIP) { + choice = (coordinate_t){(uint8_t)(index % kBoardWidth), (uint8_t)(index / kBoardWidth)}; + found = true; + break; + } + } + assert(found); + assert(game_engine_shot(&engine, player, choice, NULL) == GAME_RESULT_OK); + } + assert(engine.state.phase == PHASE_FINISHED); + assert(engine.state.winner < kPlayerCapacity); + for (uint8_t player = 0; player < kPlayerCapacity; ++player) { + const match_statistics_t *statistics = &engine.state.statistics[player]; + assert(statistics->shots == statistics->hits + statistics->misses); + assert(statistics->ships_sunk <= kFleetShipCount); + } + } +} + +int main(void) { + test_many_fleets(); + test_rules_and_rejection(); + test_sinking_and_victory(); + test_simulated_games(); + puts("game domain tests passed"); + return 0; +}