feat: implement the ESP32 opponent
This commit is contained in:
@@ -480,7 +480,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record,
|
|||||||
|
|
||||||
## Milestone 008 — Implement the ESP32 opponent
|
## Milestone 008 — Implement the ESP32 opponent
|
||||||
|
|
||||||
**Status:** `READY`
|
**Status:** `DONE`
|
||||||
**Depends on:** Milestone 007
|
**Depends on:** Milestone 007
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
@@ -506,11 +506,22 @@ Implement a fair, bounded `hunt/target` opponent that uses only information avai
|
|||||||
|
|
||||||
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 009 from `BLOCKED` to `READY`.
|
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 009 from `BLOCKED` to `READY`.
|
||||||
|
|
||||||
|
### Execution record
|
||||||
|
|
||||||
|
- Date: 2026-08-28
|
||||||
|
- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2.
|
||||||
|
- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; `esp_littlefs` 1.20.4.
|
||||||
|
- Result: PASS.
|
||||||
|
- Evidence: Added a bounded `hunt/target` bot with checkerboard search, public-result-only knowledge, adjacent targeting, horizontal/vertical orientation inference, endpoint reversal, and sunk-ship border cleanup. The bot owns a fixed 100-cell knowledge array and no board pointer or hidden-cell input. Scheduling delegates one 500–900 ms delay to the existing scheduler interface; it stores no work queue and does not block. Cancellation clears a pending bot turn.
|
||||||
|
- Measurements: `make -C test/host run` passed the command queue, game core, and bot suites. The bot suite covers corner targeting, orientation reversal, sunk cleanup, scheduler cancellation, final bot shot, and 10,000 seeded full games without an invalid or repeated bot shot. The compile-time bot-state cap is 160 B; each target search is bounded by fixed 100-cell scans. `pio run -e esp32-c6-devkitm-1` passed with 38,212 / 327,680 B RAM (11.7%) and 1,000,730 / 2,097,152 B flash (47.7%), within the Milestone 005 limits.
|
||||||
|
- Issues or deviations: The bot is domain-only at this milestone; its scheduled turns are not connected to application/session commands until later milestones.
|
||||||
|
- Next action: Milestone 009 is ready. Do not start it unless explicitly requested.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 009 — Implement sessions, lobby, roles, and rematch lifecycle
|
## Milestone 009 — Implement sessions, lobby, roles, and rematch lifecycle
|
||||||
|
|
||||||
**Status:** `BLOCKED`
|
**Status:** `READY`
|
||||||
**Depends on:** Milestone 008
|
**Depends on:** Milestone 008
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ enum {
|
|||||||
kSpectatorCapacity = 8,
|
kSpectatorCapacity = 8,
|
||||||
kSessionCapacity = kPlayerCapacity + kSpectatorCapacity,
|
kSessionCapacity = kPlayerCapacity + kSpectatorCapacity,
|
||||||
kCommandQueueCapacity = 16,
|
kCommandQueueCapacity = 16,
|
||||||
|
kBotKnowledgeCellCount = kBoardCellCount,
|
||||||
kStateMessageCapacity = 512,
|
kStateMessageCapacity = 512,
|
||||||
kRequestBodyCapacity = 192,
|
kRequestBodyCapacity = 192,
|
||||||
kSessionTokenBytes = 16,
|
kSessionTokenBytes = 16,
|
||||||
|
|||||||
+22
-2
@@ -1,5 +1,25 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_interfaces.h"
|
#include <stdbool.h>
|
||||||
|
|
||||||
typedef struct { random_source_t random; scheduler_t scheduler; } bot_player_t;
|
#include "app_interfaces.h"
|
||||||
|
#include "game_types.h"
|
||||||
|
|
||||||
|
typedef uint8_t bot_cell_t;
|
||||||
|
enum { BOT_CELL_UNKNOWN, BOT_CELL_MISS, BOT_CELL_HIT, BOT_CELL_BLOCKED };
|
||||||
|
typedef struct { bool hit; bool sunk; } bot_shot_result_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
random_source_t random;
|
||||||
|
scheduler_t scheduler;
|
||||||
|
bot_cell_t knowledge[kBotKnowledgeCellCount];
|
||||||
|
bool turn_pending;
|
||||||
|
} bot_player_t;
|
||||||
|
|
||||||
|
void bot_player_init(bot_player_t *bot, random_source_t random, scheduler_t scheduler);
|
||||||
|
bool bot_player_next_shot(bot_player_t *bot, coordinate_t *coordinate);
|
||||||
|
void bot_player_record_result(bot_player_t *bot, coordinate_t coordinate, const bot_shot_result_t *result);
|
||||||
|
bool bot_player_schedule_turn(bot_player_t *bot);
|
||||||
|
void bot_player_cancel_turn(bot_player_t *bot);
|
||||||
|
|
||||||
|
_Static_assert(sizeof(bot_player_t) <= 160, "bot state grew beyond fixed budget");
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# without default 'CMakeLists.txt' file.
|
# without default 'CMakeLists.txt' file.
|
||||||
|
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.c" "application.c" "command_queue.c" "fleet_generator.c" "game_engine.c"
|
SRCS "main.c" "application.c" "command_queue.c" "fleet_generator.c" "game_engine.c" "bot_player.c"
|
||||||
INCLUDE_DIRS "../include"
|
INCLUDE_DIRS "../include"
|
||||||
REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
|
REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#include "bot_player.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static uint8_t cell_index(uint8_t x, uint8_t y) {
|
||||||
|
return (uint8_t)(y * kBoardWidth + x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool coordinate_is_valid(coordinate_t coordinate) {
|
||||||
|
return coordinate.x < kBoardWidth && coordinate.y < kBoardHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t random_u32(bot_player_t *bot) {
|
||||||
|
if (bot->random.next_u32 == NULL) return 0U;
|
||||||
|
return bot->random.next_u32(bot->random.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool choose_unknown(const bot_player_t *bot, uint8_t start, bool checkerboard,
|
||||||
|
coordinate_t *coordinate) {
|
||||||
|
for (uint8_t offset = 0; offset < kBotKnowledgeCellCount; ++offset) {
|
||||||
|
const uint8_t index = (uint8_t)((start + offset) % kBotKnowledgeCellCount);
|
||||||
|
const uint8_t x = (uint8_t)(index % kBoardWidth);
|
||||||
|
const uint8_t y = (uint8_t)(index / kBoardWidth);
|
||||||
|
if (bot->knowledge[index] != BOT_CELL_UNKNOWN) continue;
|
||||||
|
if (checkerboard && ((x + y) & 1U) != 0U) continue;
|
||||||
|
*coordinate = (coordinate_t){x, y};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t collect_hit_group(const bot_player_t *bot, uint8_t first, uint8_t *group,
|
||||||
|
uint8_t *min_x, uint8_t *max_x, uint8_t *min_y, uint8_t *max_y) {
|
||||||
|
uint8_t queue[kBotKnowledgeCellCount] = {0};
|
||||||
|
uint8_t count = 0;
|
||||||
|
uint8_t read = 0;
|
||||||
|
queue[count++] = first;
|
||||||
|
group[0] = first;
|
||||||
|
*min_x = *max_x = (uint8_t)(first % kBoardWidth);
|
||||||
|
*min_y = *max_y = (uint8_t)(first / kBoardWidth);
|
||||||
|
while (read < count) {
|
||||||
|
const uint8_t index = queue[read++];
|
||||||
|
const uint8_t x = (uint8_t)(index % kBoardWidth);
|
||||||
|
const uint8_t y = (uint8_t)(index / kBoardWidth);
|
||||||
|
const int8_t directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
|
||||||
|
for (uint8_t direction = 0; direction < 4; ++direction) {
|
||||||
|
const int16_t neighbour_x = (int16_t)x + directions[direction][0];
|
||||||
|
const int16_t neighbour_y = (int16_t)y + directions[direction][1];
|
||||||
|
if (neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= kBoardWidth || neighbour_y >= kBoardHeight) continue;
|
||||||
|
const uint8_t neighbour = cell_index((uint8_t)neighbour_x, (uint8_t)neighbour_y);
|
||||||
|
if (bot->knowledge[neighbour] != BOT_CELL_HIT) continue;
|
||||||
|
bool already_present = false;
|
||||||
|
for (uint8_t item = 0; item < count; ++item) {
|
||||||
|
if (queue[item] == neighbour) {
|
||||||
|
already_present = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (already_present) continue;
|
||||||
|
queue[count] = neighbour;
|
||||||
|
group[count++] = neighbour;
|
||||||
|
if ((uint8_t)neighbour_x < *min_x) *min_x = (uint8_t)neighbour_x;
|
||||||
|
if ((uint8_t)neighbour_x > *max_x) *max_x = (uint8_t)neighbour_x;
|
||||||
|
if ((uint8_t)neighbour_y < *min_y) *min_y = (uint8_t)neighbour_y;
|
||||||
|
if ((uint8_t)neighbour_y > *max_y) *max_y = (uint8_t)neighbour_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool choose_target(bot_player_t *bot, coordinate_t *coordinate) {
|
||||||
|
uint8_t first = kBotKnowledgeCellCount;
|
||||||
|
for (uint8_t index = 0; index < kBotKnowledgeCellCount; ++index) {
|
||||||
|
if (bot->knowledge[index] == BOT_CELL_HIT) {
|
||||||
|
first = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first == kBotKnowledgeCellCount) return false;
|
||||||
|
|
||||||
|
uint8_t group[kBotKnowledgeCellCount] = {0};
|
||||||
|
uint8_t min_x = 0;
|
||||||
|
uint8_t max_x = 0;
|
||||||
|
uint8_t min_y = 0;
|
||||||
|
uint8_t max_y = 0;
|
||||||
|
const uint8_t count = collect_hit_group(bot, first, group, &min_x, &max_x, &min_y, &max_y);
|
||||||
|
if (count >= 2U && min_y == max_y) {
|
||||||
|
const coordinate_t ends[2] = {{min_x == 0 ? kBoardWidth : (uint8_t)(min_x - 1U), min_y},
|
||||||
|
{max_x + 1U, min_y}};
|
||||||
|
const uint8_t first_end = (uint8_t)(random_u32(bot) & 1U);
|
||||||
|
for (uint8_t offset = 0; offset < 2; ++offset) {
|
||||||
|
const coordinate_t candidate = ends[(first_end + offset) & 1U];
|
||||||
|
if (coordinate_is_valid(candidate) && bot->knowledge[cell_index(candidate.x, candidate.y)] == BOT_CELL_UNKNOWN) {
|
||||||
|
*coordinate = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (count >= 2U && min_x == max_x) {
|
||||||
|
const coordinate_t ends[2] = {{min_x, min_y == 0 ? kBoardHeight : (uint8_t)(min_y - 1U)},
|
||||||
|
{min_x, max_y + 1U}};
|
||||||
|
const uint8_t first_end = (uint8_t)(random_u32(bot) & 1U);
|
||||||
|
for (uint8_t offset = 0; offset < 2; ++offset) {
|
||||||
|
const coordinate_t candidate = ends[(first_end + offset) & 1U];
|
||||||
|
if (coordinate_is_valid(candidate) && bot->knowledge[cell_index(candidate.x, candidate.y)] == BOT_CELL_UNKNOWN) {
|
||||||
|
*coordinate = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t hit_x = (uint8_t)(first % kBoardWidth);
|
||||||
|
const uint8_t hit_y = (uint8_t)(first / kBoardWidth);
|
||||||
|
const int8_t directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
|
||||||
|
const uint8_t first_direction = (uint8_t)(random_u32(bot) % 4U);
|
||||||
|
for (uint8_t offset = 0; offset < 4; ++offset) {
|
||||||
|
const int8_t *direction = directions[(first_direction + offset) % 4U];
|
||||||
|
const int16_t x = (int16_t)hit_x + direction[0];
|
||||||
|
const int16_t y = (int16_t)hit_y + direction[1];
|
||||||
|
if (x < 0 || y < 0 || x >= kBoardWidth || y >= kBoardHeight) continue;
|
||||||
|
if (bot->knowledge[cell_index((uint8_t)x, (uint8_t)y)] == BOT_CELL_UNKNOWN) {
|
||||||
|
*coordinate = (coordinate_t){(uint8_t)x, (uint8_t)y};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bot_player_init(bot_player_t *bot, random_source_t random, scheduler_t scheduler) {
|
||||||
|
if (bot == NULL) return;
|
||||||
|
memset(bot, 0, sizeof(*bot));
|
||||||
|
bot->random = random;
|
||||||
|
bot->scheduler = scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bot_player_next_shot(bot_player_t *bot, coordinate_t *coordinate) {
|
||||||
|
if (bot == NULL || coordinate == NULL) return false;
|
||||||
|
bot->turn_pending = false;
|
||||||
|
if (choose_target(bot, coordinate)) return true;
|
||||||
|
const uint8_t start = (uint8_t)(random_u32(bot) % kBotKnowledgeCellCount);
|
||||||
|
return choose_unknown(bot, start, true, coordinate) || choose_unknown(bot, start, false, coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bot_player_record_result(bot_player_t *bot, coordinate_t coordinate, const bot_shot_result_t *result) {
|
||||||
|
if (bot == NULL || result == NULL || !coordinate_is_valid(coordinate)) return;
|
||||||
|
const uint8_t index = cell_index(coordinate.x, coordinate.y);
|
||||||
|
bot->knowledge[index] = result->hit ? BOT_CELL_HIT : BOT_CELL_MISS;
|
||||||
|
if (!result->sunk || !result->hit) return;
|
||||||
|
|
||||||
|
uint8_t group[kBotKnowledgeCellCount] = {0};
|
||||||
|
uint8_t min_x = 0;
|
||||||
|
uint8_t max_x = 0;
|
||||||
|
uint8_t min_y = 0;
|
||||||
|
uint8_t max_y = 0;
|
||||||
|
const uint8_t count = collect_hit_group(bot, index, group, &min_x, &max_x, &min_y, &max_y);
|
||||||
|
for (uint8_t item = 0; item < count; ++item) bot->knowledge[group[item]] = BOT_CELL_BLOCKED;
|
||||||
|
const uint8_t from_x = min_x == 0 ? 0 : (uint8_t)(min_x - 1U);
|
||||||
|
const uint8_t from_y = min_y == 0 ? 0 : (uint8_t)(min_y - 1U);
|
||||||
|
const uint8_t to_x = max_x + 1U >= kBoardWidth ? (uint8_t)(kBoardWidth - 1U) : (uint8_t)(max_x + 1U);
|
||||||
|
const uint8_t to_y = max_y + 1U >= kBoardHeight ? (uint8_t)(kBoardHeight - 1U) : (uint8_t)(max_y + 1U);
|
||||||
|
for (uint8_t y = from_y; y <= to_y; ++y) {
|
||||||
|
for (uint8_t x = from_x; x <= to_x; ++x) {
|
||||||
|
const uint8_t neighbour = cell_index(x, y);
|
||||||
|
if (bot->knowledge[neighbour] == BOT_CELL_UNKNOWN) bot->knowledge[neighbour] = BOT_CELL_BLOCKED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bot_player_schedule_turn(bot_player_t *bot) {
|
||||||
|
if (bot == NULL || bot->turn_pending || bot->scheduler.schedule_after_ms == NULL) return false;
|
||||||
|
const uint32_t delay_ms = 500U + (random_u32(bot) % 401U);
|
||||||
|
if (!bot->scheduler.schedule_after_ms(bot->scheduler.context, delay_ms)) return false;
|
||||||
|
bot->turn_pending = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bot_player_cancel_turn(bot_player_t *bot) {
|
||||||
|
if (bot != NULL) bot->turn_pending = false;
|
||||||
|
}
|
||||||
+6
-2
@@ -1,7 +1,7 @@
|
|||||||
CC ?= cc
|
CC ?= cc
|
||||||
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
|
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
|
test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/application.c
|
||||||
$(CC) $(CFLAGS) $^ -o $@
|
$(CC) $(CFLAGS) $^ -o $@
|
||||||
@@ -9,9 +9,13 @@ test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/app
|
|||||||
run: all
|
run: all
|
||||||
./test_command_queue
|
./test_command_queue
|
||||||
./test_game_domain
|
./test_game_domain
|
||||||
|
./test_bot_player
|
||||||
|
|
||||||
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
|
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
|
||||||
$(CC) $(CFLAGS) $^ -o $@
|
$(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:
|
clean:
|
||||||
rm -f test_command_queue test_game_domain
|
rm -f test_command_queue test_game_domain test_bot_player
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user