feat: implement sessions, lobby, roles, and rematch lifecycle
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
# without default 'CMakeLists.txt' file.
|
||||
|
||||
idf_component_register(
|
||||
SRCS "main.c" "application.c" "command_queue.c" "fleet_generator.c" "game_engine.c" "bot_player.c"
|
||||
SRCS "main.c" "application.c" "command_queue.c" "fleet_generator.c" "game_engine.c" "bot_player.c" "session_manager.c" "game_lifecycle.c"
|
||||
INCLUDE_DIRS "../include"
|
||||
REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
|
||||
)
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "game_lifecycle.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
static lifecycle_result_t session_result(session_result_t result) {
|
||||
switch (result) {
|
||||
case SESSION_RESULT_OK: return LIFECYCLE_RESULT_OK;
|
||||
case SESSION_RESULT_INVALID_NAME: return LIFECYCLE_RESULT_INVALID_NAME;
|
||||
case SESSION_RESULT_INVALID_ROLE: return LIFECYCLE_RESULT_INVALID_ROLE;
|
||||
case SESSION_RESULT_NO_PLAYER_SLOT: return LIFECYCLE_RESULT_NO_PLAYER_SLOT;
|
||||
case SESSION_RESULT_NO_SPECTATOR_SLOT: return LIFECYCLE_RESULT_NO_SPECTATOR_SLOT;
|
||||
case SESSION_RESULT_WRONG_PHASE: return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
default: return LIFECYCLE_RESULT_UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
static lifecycle_result_t game_result(game_result_t result) {
|
||||
switch (result) {
|
||||
case GAME_RESULT_OK: return LIFECYCLE_RESULT_OK;
|
||||
case GAME_RESULT_INVALID_COORDINATE: return LIFECYCLE_RESULT_INVALID_COORDINATE;
|
||||
case GAME_RESULT_WRONG_PHASE: return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
case GAME_RESULT_NOT_YOUR_TURN: return LIFECYCLE_RESULT_NOT_YOUR_TURN;
|
||||
case GAME_RESULT_CELL_ALREADY_SHOT: return LIFECYCLE_RESULT_CELL_ALREADY_SHOT;
|
||||
default: return LIFECYCLE_RESULT_GENERATION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_player(const game_lifecycle_t *lifecycle, uint8_t session_index, uint8_t *player) {
|
||||
if (lifecycle == NULL || session_index >= kPlayerCapacity || !lifecycle->sessions.entries[session_index].occupied) return false;
|
||||
if (lifecycle->sessions.entries[session_index].role != session_index) return false;
|
||||
if (player != NULL) *player = session_index;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool game_id_matches(const game_lifecycle_t *lifecycle, uint32_t game_id) {
|
||||
return lifecycle != NULL && lifecycle->game.state.game_id == game_id;
|
||||
}
|
||||
|
||||
static void record_cumulative(game_lifecycle_t *lifecycle) {
|
||||
for (uint8_t player = 0; player < kPlayerCapacity; ++player) {
|
||||
cumulative_statistics_t *total = &lifecycle->cumulative[player];
|
||||
const match_statistics_t *match = &lifecycle->game.state.statistics[player];
|
||||
++total->games;
|
||||
total->shots += match->shots;
|
||||
total->hits += match->hits;
|
||||
if (lifecycle->game.state.winner == player) ++total->wins;
|
||||
else ++total->losses;
|
||||
}
|
||||
}
|
||||
|
||||
static lifecycle_result_t start_current_game(game_lifecycle_t *lifecycle) {
|
||||
const uint32_t game_id = lifecycle->game.state.game_id;
|
||||
const game_mode_t mode = lifecycle->game.state.mode;
|
||||
const uint32_t version = lifecycle->game.state.version;
|
||||
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}));
|
||||
}
|
||||
|
||||
void game_lifecycle_init(game_lifecycle_t *lifecycle, random_source_t random) {
|
||||
if (lifecycle == NULL) return;
|
||||
memset(lifecycle, 0, sizeof(*lifecycle));
|
||||
lifecycle->random = random;
|
||||
lifecycle->next_game_id = 2U;
|
||||
game_engine_init(&lifecycle->game);
|
||||
lifecycle->game.state.game_id = 1U;
|
||||
session_manager_init(&lifecycle->sessions);
|
||||
}
|
||||
|
||||
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;
|
||||
if ((requested_role == ROLE_PLAYER_1 || requested_role == ROLE_PLAYER_2) && lifecycle->game.state.phase != PHASE_LOBBY) {
|
||||
return LIFECYCLE_RESULT_NO_PLAYER_SLOT;
|
||||
}
|
||||
if (requested_role == ROLE_PLAYER_2 && !session_manager_player_present(&lifecycle->sessions, 0U)) {
|
||||
return LIFECYCLE_RESULT_NO_PLAYER_SLOT;
|
||||
}
|
||||
if (requested_role == ROLE_PLAYER_2 && (lifecycle->game.state.mode == MODE_BOT || lifecycle->bot_reserved)) {
|
||||
return LIFECYCLE_RESULT_NO_PLAYER_SLOT;
|
||||
}
|
||||
return session_result(session_manager_join(&lifecycle->sessions, requested_role, name, lifecycle->random, session_index));
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_resume(game_lifecycle_t *lifecycle,
|
||||
const uint8_t token[kSessionTokenBytes], uint8_t *session_index) {
|
||||
return lifecycle == NULL ? LIFECYCLE_RESULT_UNAUTHORIZED :
|
||||
session_result(session_manager_resume(&lifecycle->sessions, token, session_index));
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_disconnect(game_lifecycle_t *lifecycle, uint8_t session_index) {
|
||||
return lifecycle != NULL && session_manager_disconnect(&lifecycle->sessions, session_index) ?
|
||||
LIFECYCLE_RESULT_OK : LIFECYCLE_RESULT_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_leave(game_lifecycle_t *lifecycle, uint8_t session_index) {
|
||||
return lifecycle == NULL ? LIFECYCLE_RESULT_UNAUTHORIZED :
|
||||
session_result(session_manager_leave(&lifecycle->sessions, session_index, lifecycle->game.state.phase));
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_configure(game_lifecycle_t *lifecycle, uint8_t session_index,
|
||||
uint32_t game_id, game_mode_t mode) {
|
||||
uint8_t player = 0;
|
||||
if (!is_player(lifecycle, session_index, &player)) return LIFECYCLE_RESULT_FORBIDDEN_ROLE;
|
||||
if (player != 0U) return LIFECYCLE_RESULT_FORBIDDEN_ROLE;
|
||||
if (!game_id_matches(lifecycle, game_id)) return LIFECYCLE_RESULT_STALE_GAME;
|
||||
if (lifecycle->game.state.phase != PHASE_LOBBY) return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
if (mode != MODE_HUMAN && mode != MODE_BOT) return LIFECYCLE_RESULT_INVALID_MODE;
|
||||
if (mode == MODE_BOT && session_manager_player_present(&lifecycle->sessions, 1U)) return LIFECYCLE_RESULT_NO_PLAYER_SLOT;
|
||||
lifecycle->game.state.mode = mode;
|
||||
lifecycle->bot_reserved = mode == MODE_BOT;
|
||||
++lifecycle->game.state.version;
|
||||
return LIFECYCLE_RESULT_OK;
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_start(game_lifecycle_t *lifecycle, uint8_t session_index, uint32_t game_id) {
|
||||
uint8_t player = 0;
|
||||
if (!is_player(lifecycle, session_index, &player)) return LIFECYCLE_RESULT_FORBIDDEN_ROLE;
|
||||
if (player != 0U) return LIFECYCLE_RESULT_FORBIDDEN_ROLE;
|
||||
if (!game_id_matches(lifecycle, game_id)) return LIFECYCLE_RESULT_STALE_GAME;
|
||||
if (lifecycle->game.state.phase != PHASE_LOBBY) return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
if (lifecycle->game.state.mode == MODE_HUMAN && !session_manager_player_present(&lifecycle->sessions, 1U)) return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
if (lifecycle->game.state.mode == MODE_BOT && !lifecycle->bot_reserved) return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
return start_current_game(lifecycle);
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_shot(game_lifecycle_t *lifecycle, uint8_t session_index, uint32_t game_id,
|
||||
coordinate_t coordinate, shot_result_t *result) {
|
||||
uint8_t player = 0;
|
||||
if (!is_player(lifecycle, session_index, &player)) return LIFECYCLE_RESULT_FORBIDDEN_ROLE;
|
||||
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);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_rematch(game_lifecycle_t *lifecycle, uint8_t session_index, uint32_t game_id) {
|
||||
uint8_t player = 0;
|
||||
if (!is_player(lifecycle, session_index, &player)) return LIFECYCLE_RESULT_FORBIDDEN_ROLE;
|
||||
if (!game_id_matches(lifecycle, game_id)) return LIFECYCLE_RESULT_STALE_GAME;
|
||||
if (lifecycle->game.state.phase != PHASE_FINISHED && lifecycle->game.state.phase != PHASE_REMATCH_WAIT) return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
lifecycle->rematch_confirmed[player] = true;
|
||||
const bool ready = lifecycle->game.state.mode == MODE_BOT ? lifecycle->rematch_confirmed[0] :
|
||||
lifecycle->rematch_confirmed[0] && lifecycle->rematch_confirmed[1];
|
||||
if (!ready) {
|
||||
if (lifecycle->game.state.phase == PHASE_FINISHED) {
|
||||
lifecycle->game.state.phase = PHASE_REMATCH_WAIT;
|
||||
++lifecycle->game.state.version;
|
||||
}
|
||||
return LIFECYCLE_RESULT_OK;
|
||||
}
|
||||
lifecycle->rematch_confirmed[0] = false;
|
||||
lifecycle->rematch_confirmed[1] = false;
|
||||
lifecycle->game.state.game_id = lifecycle->next_game_id++;
|
||||
return start_current_game(lifecycle);
|
||||
}
|
||||
|
||||
lifecycle_result_t game_lifecycle_abort(game_lifecycle_t *lifecycle, uint8_t session_index, uint32_t game_id) {
|
||||
uint8_t player = 0;
|
||||
if (!is_player(lifecycle, session_index, &player) || player != 0U) return LIFECYCLE_RESULT_FORBIDDEN_ROLE;
|
||||
if (!game_id_matches(lifecycle, game_id)) return LIFECYCLE_RESULT_STALE_GAME;
|
||||
if (lifecycle->game.state.phase != PHASE_IN_PROGRESS || lifecycle->game.state.mode != MODE_HUMAN ||
|
||||
lifecycle->sessions.entries[1].connected) return LIFECYCLE_RESULT_WRONG_PHASE;
|
||||
const uint32_t version = lifecycle->game.state.version + 1U;
|
||||
const uint32_t next_game_id = lifecycle->next_game_id++;
|
||||
game_engine_init(&lifecycle->game);
|
||||
lifecycle->game.state.game_id = next_game_id;
|
||||
lifecycle->game.state.version = version;
|
||||
lifecycle->bot_reserved = false;
|
||||
return LIFECYCLE_RESULT_OK;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "session_manager.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
static bool valid_utf8_scalar(const unsigned char *text, size_t remaining, size_t *bytes) {
|
||||
if (text[0] < 0x80U) { *bytes = 1; return text[0] >= 0x20U && text[0] != '<' && text[0] != '>'; }
|
||||
if (text[0] >= 0xC2U && text[0] <= 0xDFU && remaining >= 2 && (text[1] & 0xC0U) == 0x80U) { *bytes = 2; return true; }
|
||||
if (text[0] >= 0xE0U && text[0] <= 0xEFU && remaining >= 3 && (text[1] & 0xC0U) == 0x80U && (text[2] & 0xC0U) == 0x80U) { *bytes = 3; return true; }
|
||||
if (text[0] >= 0xF0U && text[0] <= 0xF4U && remaining >= 4 && (text[1] & 0xC0U) == 0x80U && (text[2] & 0xC0U) == 0x80U && (text[3] & 0xC0U) == 0x80U) { *bytes = 4; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool sanitize_name(const char *name, char output[kDisplayNameBytes + 1]) {
|
||||
if (name == NULL) return false;
|
||||
size_t input = 0;
|
||||
size_t written = 0;
|
||||
uint8_t scalars = 0;
|
||||
while (name[input] != '\0') {
|
||||
size_t bytes = 0;
|
||||
if (!valid_utf8_scalar((const unsigned char *)&name[input], strlen(&name[input]), &bytes)) return false;
|
||||
if (written + bytes > kDisplayNameBytes || ++scalars > 20U) return false;
|
||||
memcpy(&output[written], &name[input], bytes);
|
||||
input += bytes;
|
||||
written += bytes;
|
||||
}
|
||||
output[written] = '\0';
|
||||
return scalars != 0U;
|
||||
}
|
||||
|
||||
static int8_t first_free(const session_manager_t *manager, uint8_t start, uint8_t end) {
|
||||
for (uint8_t index = start; index < end; ++index) if (!manager->entries[index].occupied) return (int8_t)index;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void session_manager_init(session_manager_t *manager) {
|
||||
if (manager != NULL) memset(manager, 0, sizeof(*manager));
|
||||
}
|
||||
|
||||
session_result_t session_manager_join(session_manager_t *manager, role_t requested_role, const char *name,
|
||||
random_source_t random, uint8_t *session_index) {
|
||||
if (manager == NULL || session_index == NULL || random.next_u32 == NULL) return SESSION_RESULT_UNAUTHORIZED;
|
||||
char sanitized[kDisplayNameBytes + 1] = {0};
|
||||
if (!sanitize_name(name, sanitized)) return SESSION_RESULT_INVALID_NAME;
|
||||
int8_t selected = -1;
|
||||
if (requested_role == ROLE_PLAYER_1) selected = first_free(manager, 0, 1);
|
||||
else if (requested_role == ROLE_PLAYER_2) selected = first_free(manager, 1, 2);
|
||||
else if (requested_role == ROLE_SPECTATOR) selected = first_free(manager, kPlayerCapacity, kSessionCapacity);
|
||||
else return SESSION_RESULT_INVALID_ROLE;
|
||||
if (selected < 0) return requested_role == ROLE_SPECTATOR ? SESSION_RESULT_NO_SPECTATOR_SLOT : SESSION_RESULT_NO_PLAYER_SLOT;
|
||||
session_t candidate = {.role = requested_role, .occupied = true, .connected = true};
|
||||
memcpy(candidate.name, sanitized, sizeof(candidate.name));
|
||||
for (uint8_t offset = 0; offset < kSessionTokenBytes; offset += 4) {
|
||||
const uint32_t value = random.next_u32(random.context);
|
||||
for (uint8_t byte = 0; byte < 4; ++byte) candidate.token[offset + byte] = (uint8_t)(value >> (byte * 8U));
|
||||
}
|
||||
manager->entries[selected] = candidate;
|
||||
*session_index = (uint8_t)selected;
|
||||
return SESSION_RESULT_OK;
|
||||
}
|
||||
|
||||
session_result_t session_manager_resume(session_manager_t *manager, const uint8_t token[kSessionTokenBytes], uint8_t *session_index) {
|
||||
if (manager == NULL || token == NULL || session_index == NULL) return SESSION_RESULT_UNAUTHORIZED;
|
||||
for (uint8_t index = 0; index < kSessionCapacity; ++index) {
|
||||
session_t *session = &manager->entries[index];
|
||||
if (session->occupied && memcmp(session->token, token, kSessionTokenBytes) == 0) {
|
||||
session->connected = true;
|
||||
*session_index = index;
|
||||
return SESSION_RESULT_OK;
|
||||
}
|
||||
}
|
||||
return SESSION_RESULT_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
bool session_manager_disconnect(session_manager_t *manager, uint8_t session_index) {
|
||||
if (manager == NULL || session_index >= kSessionCapacity || !manager->entries[session_index].occupied) return false;
|
||||
if (manager->entries[session_index].role == ROLE_SPECTATOR) memset(&manager->entries[session_index], 0, sizeof(session_t));
|
||||
else manager->entries[session_index].connected = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
session_result_t session_manager_leave(session_manager_t *manager, uint8_t session_index, phase_t phase) {
|
||||
if (manager == NULL || session_index >= kSessionCapacity || !manager->entries[session_index].occupied) return SESSION_RESULT_UNAUTHORIZED;
|
||||
if (manager->entries[session_index].role != ROLE_SPECTATOR && phase != PHASE_LOBBY) return SESSION_RESULT_WRONG_PHASE;
|
||||
memset(&manager->entries[session_index], 0, sizeof(session_t));
|
||||
return SESSION_RESULT_OK;
|
||||
}
|
||||
|
||||
bool session_manager_player_present(const session_manager_t *manager, uint8_t player_index) {
|
||||
return manager != NULL && player_index < kPlayerCapacity && manager->entries[player_index].occupied;
|
||||
}
|
||||
Reference in New Issue
Block a user