feat: implement sessions, lobby, roles, and rematch lifecycle

This commit is contained in:
2026-08-28 22:57:17 +03:00
parent ec1ff20ddd
commit a691783f58
10 changed files with 488 additions and 9 deletions
+174
View File
@@ -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;
}