feat: complete human-vs-ESP32 gameplay and cumulative statistics
This commit is contained in:
@@ -8,6 +8,10 @@ void application_init(application_t *application, random_source_t random) {
|
||||
game_lifecycle_init(&application->lifecycle, random);
|
||||
}
|
||||
|
||||
void application_set_bot_scheduler(application_t *application, scheduler_t scheduler) {
|
||||
if (application != NULL) game_lifecycle_set_bot_scheduler(&application->lifecycle, scheduler);
|
||||
}
|
||||
|
||||
bool application_enqueue(application_t *application, const app_command_t *command) {
|
||||
return application != NULL && command_queue_push(&application->command_queue, command);
|
||||
}
|
||||
@@ -52,3 +56,7 @@ lifecycle_result_t application_submit(application_t *application, const app_comm
|
||||
return LIFECYCLE_RESULT_GENERATION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
bool application_bot_take_turn(application_t *application) {
|
||||
return application != NULL && game_lifecycle_bot_take_turn(&application->lifecycle);
|
||||
}
|
||||
|
||||
+31
-2
@@ -44,6 +44,8 @@ static void record_cumulative(game_lifecycle_t *lifecycle) {
|
||||
++total->games;
|
||||
total->shots += match->shots;
|
||||
total->hits += match->hits;
|
||||
total->misses += match->misses;
|
||||
total->ships_sunk += match->ships_sunk;
|
||||
if (lifecycle->game.state.winner == player) ++total->wins;
|
||||
else ++total->losses;
|
||||
}
|
||||
@@ -56,8 +58,14 @@ static lifecycle_result_t start_current_game(game_lifecycle_t *lifecycle) {
|
||||
game_engine_init(&lifecycle->game);
|
||||
lifecycle->game.state.game_id = game_id;
|
||||
lifecycle->game.state.version = version;
|
||||
return game_result(game_engine_start(&lifecycle->game, game_id, mode,
|
||||
&(fleet_generator_t){.random = lifecycle->random}));
|
||||
const lifecycle_result_t result = game_result(game_engine_start(&lifecycle->game, game_id, mode,
|
||||
&(fleet_generator_t){.random = lifecycle->random}));
|
||||
if (result != LIFECYCLE_RESULT_OK || mode != MODE_BOT) return result;
|
||||
bot_player_init(&lifecycle->bot, lifecycle->random, lifecycle->bot_scheduler);
|
||||
if (lifecycle->game.state.current_player == 1U && lifecycle->bot_scheduler.schedule_after_ms != NULL) {
|
||||
(void)bot_player_schedule_turn(&lifecycle->bot);
|
||||
}
|
||||
return LIFECYCLE_RESULT_OK;
|
||||
}
|
||||
|
||||
void game_lifecycle_init(game_lifecycle_t *lifecycle, random_source_t random) {
|
||||
@@ -70,6 +78,11 @@ void game_lifecycle_init(game_lifecycle_t *lifecycle, random_source_t random) {
|
||||
session_manager_init(&lifecycle->sessions);
|
||||
}
|
||||
|
||||
void game_lifecycle_set_bot_scheduler(game_lifecycle_t *lifecycle, scheduler_t scheduler) {
|
||||
if (lifecycle == NULL) return;
|
||||
lifecycle->bot_scheduler = scheduler;
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_join(game_lifecycle_t *lifecycle, role_t requested_role, const char *name,
|
||||
uint8_t *session_index) {
|
||||
if (lifecycle == NULL) return LIFECYCLE_RESULT_UNAUTHORIZED;
|
||||
@@ -134,6 +147,8 @@ lifecycle_result_t game_lifecycle_shot(game_lifecycle_t *lifecycle, uint8_t sess
|
||||
if (!game_id_matches(lifecycle, game_id)) return LIFECYCLE_RESULT_STALE_GAME;
|
||||
const lifecycle_result_t outcome = game_result(game_engine_shot(&lifecycle->game, player, coordinate, result));
|
||||
if (outcome == LIFECYCLE_RESULT_OK && lifecycle->game.state.phase == PHASE_FINISHED) record_cumulative(lifecycle);
|
||||
else if (outcome == LIFECYCLE_RESULT_OK && lifecycle->game.state.mode == MODE_BOT && lifecycle->game.state.current_player == 1U &&
|
||||
lifecycle->bot_scheduler.schedule_after_ms != NULL) (void)bot_player_schedule_turn(&lifecycle->bot);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@@ -170,5 +185,19 @@ lifecycle_result_t game_lifecycle_abort(game_lifecycle_t *lifecycle, uint8_t ses
|
||||
lifecycle->game.state.game_id = next_game_id;
|
||||
lifecycle->game.state.version = version;
|
||||
lifecycle->bot_reserved = false;
|
||||
bot_player_cancel_turn(&lifecycle->bot);
|
||||
return LIFECYCLE_RESULT_OK;
|
||||
}
|
||||
|
||||
bool game_lifecycle_bot_take_turn(game_lifecycle_t *lifecycle) {
|
||||
if (lifecycle == NULL || lifecycle->game.state.phase != PHASE_IN_PROGRESS || lifecycle->game.state.mode != MODE_BOT ||
|
||||
lifecycle->game.state.current_player != 1U) return false;
|
||||
coordinate_t coordinate = {0};
|
||||
shot_result_t result = {0};
|
||||
if (!bot_player_next_shot(&lifecycle->bot, &coordinate) ||
|
||||
game_engine_shot(&lifecycle->game, 1U, coordinate, &result) != GAME_RESULT_OK) return false;
|
||||
bot_player_record_result(&lifecycle->bot, coordinate, &(bot_shot_result_t){.hit = result.hit, .sunk = result.sunk});
|
||||
if (lifecycle->game.state.phase == PHASE_FINISHED) record_cumulative(lifecycle);
|
||||
else if (lifecycle->game.state.current_player == 1U) (void)bot_player_schedule_turn(&lifecycle->bot);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -299,6 +299,28 @@ bool http_api_handle(http_api_t *api, const http_api_request_t *request, http_ap
|
||||
} else response->status = 200U;
|
||||
return true;
|
||||
}
|
||||
if (request->route == HTTP_API_ROUTE_STATISTICS && request->method == HTTP_API_GET) {
|
||||
role_t viewer = ROLE_SPECTATOR;
|
||||
uint8_t token[kSessionTokenBytes];
|
||||
uint8_t session_index = 0;
|
||||
if (request->session_token != NULL && request->session_token[0] != '\0') {
|
||||
if (!parse_token(request->session_token, token) || !application_session_for_token(api->application, token, &session_index)) {
|
||||
response_error(response, 401U, "UNAUTHORIZED", "Сессия не найдена", version);
|
||||
return true;
|
||||
}
|
||||
viewer = lifecycle->sessions.entries[session_index].role;
|
||||
}
|
||||
const match_statistics_t *match = lifecycle->game.state.statistics;
|
||||
const cumulative_statistics_t *total = lifecycle->cumulative;
|
||||
response_write(response, 200U, "{\"ok\":true,\"viewer\":\"%s\",\"gameId\":%" PRIu32
|
||||
",\"match\":[[%u,%u,%u,%u],[%u,%u,%u,%u]],\"cumulative\":[[%u,%u,%u,%u,%" PRIu32 ",%" PRIu32 ",%" PRIu32
|
||||
"],[%u,%u,%u,%u,%" PRIu32 ",%" PRIu32 ",%" PRIu32 "]]}", role_text(viewer), lifecycle->game.state.game_id,
|
||||
match[0].shots, match[0].hits, match[0].misses, match[0].ships_sunk,
|
||||
match[1].shots, match[1].hits, match[1].misses, match[1].ships_sunk,
|
||||
total[0].games, total[0].wins, total[0].losses, total[0].ships_sunk, total[0].shots, total[0].hits, total[0].misses,
|
||||
total[1].games, total[1].wins, total[1].losses, total[1].ships_sunk, total[1].shots, total[1].hits, total[1].misses);
|
||||
return response->body_length < sizeof(response->body);
|
||||
}
|
||||
if (request->route == HTTP_API_ROUTE_JOIN) {
|
||||
char name[kDisplayNameBytes + 1U] = {0};
|
||||
role_t role = ROLE_SPECTATOR;
|
||||
|
||||
+23
-2
@@ -44,6 +44,7 @@ static http_api_t s_api;
|
||||
static sync_service_t s_sync;
|
||||
static uint16_t s_rejected_input;
|
||||
static esp_timer_handle_t s_sync_timer;
|
||||
static esp_timer_handle_t s_bot_timer;
|
||||
#if WIFI_CONFIG_AVAILABLE
|
||||
static esp_timer_handle_t s_reconnect_timer;
|
||||
#endif
|
||||
@@ -110,6 +111,22 @@ static void queue_state_broadcast(void) {
|
||||
if (s_server != NULL && httpd_queue_work(s_server, sync_broadcast_work, NULL) != ESP_OK) ++s_rejected_input;
|
||||
}
|
||||
|
||||
static bool schedule_bot_turn(void *unused, uint32_t delay_ms) {
|
||||
(void)unused;
|
||||
return s_bot_timer != NULL && esp_timer_start_once(s_bot_timer, (uint64_t)delay_ms * 1000U) == ESP_OK;
|
||||
}
|
||||
|
||||
static void bot_turn_work(void *unused) {
|
||||
(void)unused;
|
||||
const uint32_t version_before = s_application.lifecycle.game.state.version;
|
||||
if (application_bot_take_turn(&s_application) && s_application.lifecycle.game.state.version != version_before) queue_state_broadcast();
|
||||
}
|
||||
|
||||
static void bot_timer_callback(void *unused) {
|
||||
(void)unused;
|
||||
if (s_server != NULL && httpd_queue_work(s_server, bot_turn_work, NULL) != ESP_OK) ++s_rejected_input;
|
||||
}
|
||||
|
||||
static void sync_expire_work(void *unused) {
|
||||
(void)unused;
|
||||
int closed[kSessionCapacity] = {0};
|
||||
@@ -162,7 +179,7 @@ static esp_err_t api_handler(httpd_req_t *request) {
|
||||
}
|
||||
const bool target_too_large = request->method == HTTP_GET && httpd_req_get_url_query_len(request) > 128U;
|
||||
if (target_too_large) ++s_rejected_input;
|
||||
if (route == HTTP_API_ROUTE_STATE) {
|
||||
if (route == HTTP_API_ROUTE_STATE || route == HTTP_API_ROUTE_STATISTICS) {
|
||||
const size_t token_length = httpd_req_get_hdr_value_len(request, "X-Session-Token");
|
||||
if (token_length > 0U && token_length < sizeof(token) && httpd_req_get_hdr_value_str(request, "X-Session-Token", token, sizeof(token)) != ESP_OK) token[0] = '\0';
|
||||
else if (token_length >= sizeof(token)) snprintf(token, sizeof(token), "%s", "invalid");
|
||||
@@ -328,7 +345,7 @@ static esp_err_t static_file_handler(httpd_req_t *request) {
|
||||
|
||||
static esp_err_t start_http_server(void) {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_uri_handlers = 15U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
config.max_uri_handlers = 16U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
ESP_RETURN_ON_ERROR(httpd_start(&s_server, &config), kLogTag, "http server start failed");
|
||||
const httpd_uri_t routes[] = {
|
||||
{.uri = "/", .method = HTTP_GET, .handler = root_handler},
|
||||
@@ -342,6 +359,7 @@ static esp_err_t start_http_server(void) {
|
||||
{.uri = "/api/game/rematch", .method = HTTP_POST, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_REMATCH},
|
||||
{.uri = "/api/game/abort", .method = HTTP_POST, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_ABORT},
|
||||
{.uri = "/api/state", .method = HTTP_GET, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_STATE},
|
||||
{.uri = "/api/statistics", .method = HTTP_GET, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_STATISTICS},
|
||||
{.uri = "/ws", .method = HTTP_GET, .handler = websocket_handler, .is_websocket = true},
|
||||
{.uri = "/*", .method = HTTP_GET, .handler = static_file_handler},
|
||||
};
|
||||
@@ -360,6 +378,9 @@ void app_main(void) {
|
||||
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); result = nvs_flash_init(); }
|
||||
ESP_ERROR_CHECK(result); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
application_init(&s_application, (random_source_t){.next_u32 = platform_random, .context = NULL});
|
||||
const esp_timer_create_args_t bot_timer_args = {.callback = bot_timer_callback, .name = "bot_turn"};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&bot_timer_args, &s_bot_timer));
|
||||
application_set_bot_scheduler(&s_application, (scheduler_t){.schedule_after_ms = schedule_bot_turn, .context = NULL});
|
||||
http_api_init(&s_api, &s_application);
|
||||
sync_service_init(&s_sync, &s_application);
|
||||
mount_littlefs();
|
||||
|
||||
Reference in New Issue
Block a user