161 lines
6.3 KiB
C
161 lines
6.3 KiB
C
#include <assert.h>
|
|
#include <stdio.h>
|
|
|
|
#include "bot_player.h"
|
|
#include "fleet_generator.h"
|
|
#include "game_engine.h"
|
|
|
|
typedef struct { uint32_t value; } test_random_t;
|
|
typedef struct { uint32_t delay_ms; uint8_t calls; bool accepted; } test_scheduler_t;
|
|
|
|
static uint32_t next_random(void *context) {
|
|
test_random_t *random = context;
|
|
random->value = random->value * 1664525U + 1013904223U;
|
|
return random->value;
|
|
}
|
|
|
|
static bool schedule_after(void *context, uint32_t delay_ms) {
|
|
test_scheduler_t *scheduler = context;
|
|
scheduler->delay_ms = delay_ms;
|
|
++scheduler->calls;
|
|
return scheduler->accepted;
|
|
}
|
|
|
|
static bot_player_t new_bot(test_random_t *random, test_scheduler_t *scheduler) {
|
|
bot_player_t bot;
|
|
bot_player_init(&bot, (random_source_t){.next_u32 = next_random, .context = random},
|
|
(scheduler_t){.schedule_after_ms = schedule_after, .context = scheduler});
|
|
return bot;
|
|
}
|
|
|
|
static void record(bot_player_t *bot, uint8_t x, uint8_t y, bool hit, bool sunk) {
|
|
const bot_shot_result_t result = {.hit = hit, .sunk = sunk};
|
|
bot_player_record_result(bot, (coordinate_t){x, y}, &result);
|
|
}
|
|
|
|
static bool is_adjacent(coordinate_t source, coordinate_t candidate) {
|
|
const int16_t dx = (int16_t)source.x - candidate.x;
|
|
const int16_t dy = (int16_t)source.y - candidate.y;
|
|
return (dx == 0 && (dy == 1 || dy == -1)) || (dy == 0 && (dx == 1 || dx == -1));
|
|
}
|
|
|
|
static void test_targeting_and_cleanup(void) {
|
|
test_random_t random = {.value = 3};
|
|
test_scheduler_t scheduler = {.accepted = true};
|
|
bot_player_t bot = new_bot(&random, &scheduler);
|
|
coordinate_t next = {0};
|
|
|
|
record(&bot, 0, 0, true, false);
|
|
assert(bot_player_next_shot(&bot, &next));
|
|
assert(is_adjacent((coordinate_t){0, 0}, next));
|
|
|
|
bot_player_init(&bot, (random_source_t){.next_u32 = next_random, .context = &random},
|
|
(scheduler_t){.schedule_after_ms = schedule_after, .context = &scheduler});
|
|
record(&bot, 4, 4, true, false);
|
|
record(&bot, 5, 4, true, false);
|
|
record(&bot, 6, 4, false, false);
|
|
assert(bot_player_next_shot(&bot, &next));
|
|
assert(next.x == 3U && next.y == 4U);
|
|
|
|
bot_player_init(&bot, (random_source_t){.next_u32 = next_random, .context = &random},
|
|
(scheduler_t){.schedule_after_ms = schedule_after, .context = &scheduler});
|
|
record(&bot, 0, 0, true, true);
|
|
assert(bot.knowledge[0] == BOT_CELL_BLOCKED);
|
|
assert(bot.knowledge[1] == BOT_CELL_BLOCKED);
|
|
assert(bot.knowledge[kBoardWidth] == BOT_CELL_BLOCKED);
|
|
assert(bot_player_next_shot(&bot, &next));
|
|
assert(next.x > 1U || next.y > 1U);
|
|
}
|
|
|
|
static void test_nonblocking_schedule_and_cancel(void) {
|
|
test_random_t random = {.value = 9};
|
|
test_scheduler_t scheduler = {.accepted = true};
|
|
bot_player_t bot = new_bot(&random, &scheduler);
|
|
assert(bot_player_schedule_turn(&bot));
|
|
assert(bot.turn_pending && scheduler.calls == 1U);
|
|
assert(scheduler.delay_ms >= 500U && scheduler.delay_ms <= 900U);
|
|
assert(!bot_player_schedule_turn(&bot));
|
|
bot_player_cancel_turn(&bot);
|
|
assert(!bot.turn_pending);
|
|
scheduler.accepted = false;
|
|
assert(!bot_player_schedule_turn(&bot));
|
|
assert(!bot.turn_pending);
|
|
}
|
|
|
|
static void test_final_bot_shot_uses_only_result(void) {
|
|
test_random_t random = {.value = 17};
|
|
test_scheduler_t scheduler = {.accepted = true};
|
|
bot_player_t bot = new_bot(&random, &scheduler);
|
|
for (uint8_t index = 1; index < kBoardCellCount; ++index) bot.knowledge[index] = BOT_CELL_BLOCKED;
|
|
game_engine_t engine;
|
|
game_engine_init(&engine);
|
|
engine.state.phase = PHASE_IN_PROGRESS;
|
|
engine.state.current_player = 1U;
|
|
engine.state.boards[0].ships_alive = 1U;
|
|
engine.state.boards[0].ships[0] = (ship_t){.x = 0, .y = 0, .length = 1, .horizontal = true};
|
|
engine.state.boards[0].cells[0] = CELL_SHIP;
|
|
|
|
coordinate_t target = {0, 0};
|
|
shot_result_t result = {0};
|
|
assert(bot_player_next_shot(&bot, &target));
|
|
/* The strategy sees this result, never engine.state.boards[0]. */
|
|
assert(game_engine_shot(&engine, 1U, target, &result) == GAME_RESULT_OK);
|
|
bot_player_record_result(&bot, target, &(bot_shot_result_t){.hit = result.hit, .sunk = result.sunk});
|
|
assert(result.hit && result.sunk && result.finished);
|
|
assert(engine.state.phase == PHASE_FINISHED && engine.state.winner == 1U);
|
|
}
|
|
|
|
static fleet_generator_t generator_for(test_random_t *random) {
|
|
return (fleet_generator_t){.random = {.next_u32 = next_random, .context = random}};
|
|
}
|
|
|
|
static coordinate_t first_available_target(const board_t *board) {
|
|
for (uint8_t index = 0; index < kBoardCellCount; ++index) {
|
|
if (board->cells[index] == CELL_WATER || board->cells[index] == CELL_SHIP) {
|
|
return (coordinate_t){(uint8_t)(index % kBoardWidth), (uint8_t)(index / kBoardWidth)};
|
|
}
|
|
}
|
|
assert(false);
|
|
return (coordinate_t){0, 0};
|
|
}
|
|
|
|
static void test_ten_thousand_games_without_hidden_board_access(void) {
|
|
for (uint32_t seed = 0; seed < 10000U; ++seed) {
|
|
test_random_t random = {.value = seed + 1000U};
|
|
test_scheduler_t scheduler = {.accepted = true};
|
|
const fleet_generator_t generator = generator_for(&random);
|
|
game_engine_t engine;
|
|
game_engine_init(&engine);
|
|
assert(game_engine_start(&engine, seed + 1U, MODE_BOT, &generator) == GAME_RESULT_OK);
|
|
bot_player_t bot = new_bot(&random, &scheduler);
|
|
bool bot_shot[kBoardCellCount] = {0};
|
|
for (uint16_t step = 0; step < 200U && engine.state.phase == PHASE_IN_PROGRESS; ++step) {
|
|
const uint8_t player = engine.state.current_player;
|
|
coordinate_t coordinate;
|
|
shot_result_t result = {0};
|
|
if (player == 1U) {
|
|
assert(bot_player_next_shot(&bot, &coordinate));
|
|
const uint8_t index = (uint8_t)(coordinate.y * kBoardWidth + coordinate.x);
|
|
assert(!bot_shot[index]);
|
|
bot_shot[index] = true;
|
|
assert(game_engine_shot(&engine, player, coordinate, &result) == GAME_RESULT_OK);
|
|
bot_player_record_result(&bot, coordinate, &(bot_shot_result_t){.hit = result.hit, .sunk = result.sunk});
|
|
} else {
|
|
coordinate = first_available_target(&engine.state.boards[1]);
|
|
assert(game_engine_shot(&engine, player, coordinate, &result) == GAME_RESULT_OK);
|
|
}
|
|
}
|
|
assert(engine.state.phase == PHASE_FINISHED);
|
|
assert(engine.state.winner < kPlayerCapacity);
|
|
}
|
|
}
|
|
|
|
int main(void) {
|
|
test_targeting_and_cleanup();
|
|
test_nonblocking_schedule_and_cancel();
|
|
test_final_bot_shot_uses_only_result();
|
|
test_ten_thousand_games_without_hidden_board_access();
|
|
puts("bot player tests passed");
|
|
return 0;
|
|
}
|