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
+7 -3
View File
@@ -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
+162
View File
@@ -0,0 +1,162 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#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;
}