feat: implement the ESP32 opponent

This commit is contained in:
2026-08-28 22:46:34 +03:00
parent 65eca822c0
commit ec1ff20ddd
7 changed files with 385 additions and 7 deletions
+6 -2
View File
@@ -1,7 +1,7 @@
CC ?= cc
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
all: test_command_queue test_game_domain
all: test_command_queue test_game_domain test_bot_player
test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/application.c
$(CC) $(CFLAGS) $^ -o $@
@@ -9,9 +9,13 @@ test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/app
run: all
./test_command_queue
./test_game_domain
./test_bot_player
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
test_bot_player: test_bot_player.c ../../src/bot_player.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
clean:
rm -f test_command_queue test_game_domain
rm -f test_command_queue test_game_domain test_bot_player
+160
View File
@@ -0,0 +1,160 @@
#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;
}