feat: implement and exhaustively test the game domain core
This commit is contained in:
+7
-3
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user