feat: implement and exhaustively test the game domain core

This commit is contained in:
2026-08-28 22:37:14 +03:00
parent a957d0defc
commit 65eca822c0
9 changed files with 456 additions and 8 deletions
+1 -1
View File
@@ -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
)
+119
View File
@@ -0,0 +1,119 @@
#include "fleet_generator.h"
#include <stddef.h>
#include <string.h>
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;
}
+111
View File
@@ -0,0 +1,111 @@
#include "game_engine.h"
#include <stddef.h>
#include <string.h>
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;
}