#include #include #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; }