163 lines
6.8 KiB
C
163 lines
6.8 KiB
C
#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;
|
|
}
|