feat: implement and exhaustively test the game domain core
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#include "game_engine.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
static uint8_t cell_index(coordinate_t coordinate) {
|
||||
return (uint8_t)(coordinate.y * kBoardWidth + coordinate.x);
|
||||
}
|
||||
|
||||
static bool coordinate_is_valid(coordinate_t coordinate) {
|
||||
return coordinate.x < kBoardWidth && coordinate.y < kBoardHeight;
|
||||
}
|
||||
|
||||
static ship_t *ship_at(board_t *board, coordinate_t coordinate) {
|
||||
for (uint8_t index = 0; index < kFleetShipCount; ++index) {
|
||||
ship_t *ship = &board->ships[index];
|
||||
for (uint8_t offset = 0; offset < ship->length; ++offset) {
|
||||
const uint8_t x = ship->horizontal ? (uint8_t)(ship->x + offset) : ship->x;
|
||||
const uint8_t y = ship->horizontal ? ship->y : (uint8_t)(ship->y + offset);
|
||||
if (x == coordinate.x && y == coordinate.y) return ship;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void mark_sunk_ship_border(board_t *board, const ship_t *ship) {
|
||||
const uint8_t end_x = ship->horizontal ? (uint8_t)(ship->x + ship->length - 1U) : ship->x;
|
||||
const uint8_t end_y = ship->horizontal ? ship->y : (uint8_t)(ship->y + ship->length - 1U);
|
||||
const uint8_t min_x = ship->x == 0 ? 0 : (uint8_t)(ship->x - 1U);
|
||||
const uint8_t min_y = ship->y == 0 ? 0 : (uint8_t)(ship->y - 1U);
|
||||
const uint8_t max_x = end_x + 1U >= kBoardWidth ? (uint8_t)(kBoardWidth - 1U) : (uint8_t)(end_x + 1U);
|
||||
const uint8_t max_y = end_y + 1U >= kBoardHeight ? (uint8_t)(kBoardHeight - 1U) : (uint8_t)(end_y + 1U);
|
||||
for (uint8_t y = min_y; y <= max_y; ++y) {
|
||||
for (uint8_t x = min_x; x <= max_x; ++x) {
|
||||
const uint8_t index = (uint8_t)(y * kBoardWidth + x);
|
||||
if (board->cells[index] == CELL_WATER) board->cells[index] = CELL_MISS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void game_engine_init(game_engine_t *engine) {
|
||||
if (engine == NULL) return;
|
||||
memset(engine, 0, sizeof(*engine));
|
||||
engine->state.phase = PHASE_LOBBY;
|
||||
engine->state.winner = kPlayerCapacity;
|
||||
}
|
||||
|
||||
game_result_t game_engine_start(game_engine_t *engine, uint32_t game_id, game_mode_t mode,
|
||||
const fleet_generator_t *generator) {
|
||||
if (engine == NULL || generator == NULL) return GAME_RESULT_GENERATION_FAILED;
|
||||
if (engine->state.phase != PHASE_LOBBY) return GAME_RESULT_WRONG_PHASE;
|
||||
if (mode != MODE_HUMAN && mode != MODE_BOT) return GAME_RESULT_WRONG_PHASE;
|
||||
|
||||
board_t generated[kPlayerCapacity] = {0};
|
||||
for (uint8_t player = 0; player < kPlayerCapacity; ++player) {
|
||||
if (!fleet_generator_generate(generator, &generated[player])) return GAME_RESULT_GENERATION_FAILED;
|
||||
}
|
||||
if (generator->random.next_u32 == NULL) return GAME_RESULT_GENERATION_FAILED;
|
||||
const uint8_t first_player = (uint8_t)(generator->random.next_u32(generator->random.context) % kPlayerCapacity);
|
||||
const uint32_t previous_version = engine->state.version;
|
||||
memset(&engine->state, 0, sizeof(engine->state));
|
||||
engine->state.game_id = game_id;
|
||||
engine->state.version = previous_version + 1U;
|
||||
engine->state.phase = PHASE_IN_PROGRESS;
|
||||
engine->state.mode = mode;
|
||||
engine->state.current_player = first_player;
|
||||
engine->state.winner = kPlayerCapacity;
|
||||
memcpy(engine->state.boards, generated, sizeof(generated));
|
||||
return GAME_RESULT_OK;
|
||||
}
|
||||
|
||||
game_result_t game_engine_shot(game_engine_t *engine, uint8_t player, coordinate_t coordinate,
|
||||
shot_result_t *result) {
|
||||
if (engine == NULL || !coordinate_is_valid(coordinate)) return GAME_RESULT_INVALID_COORDINATE;
|
||||
if (engine->state.phase != PHASE_IN_PROGRESS) return GAME_RESULT_WRONG_PHASE;
|
||||
if (player >= kPlayerCapacity || player != engine->state.current_player) return GAME_RESULT_NOT_YOUR_TURN;
|
||||
board_t *target = &engine->state.boards[player ^ 1U];
|
||||
cell_t *cell = &target->cells[cell_index(coordinate)];
|
||||
if (*cell == CELL_MISS || *cell == CELL_HIT) return GAME_RESULT_CELL_ALREADY_SHOT;
|
||||
|
||||
shot_result_t accepted = {0};
|
||||
match_statistics_t *statistics = &engine->state.statistics[player];
|
||||
++statistics->shots;
|
||||
if (*cell == CELL_SHIP) {
|
||||
*cell = CELL_HIT;
|
||||
++statistics->hits;
|
||||
accepted.hit = true;
|
||||
ship_t *ship = ship_at(target, coordinate);
|
||||
if (ship != NULL) {
|
||||
++ship->hits;
|
||||
if (ship->hits == ship->length) {
|
||||
--target->ships_alive;
|
||||
++statistics->ships_sunk;
|
||||
accepted.sunk = true;
|
||||
mark_sunk_ship_border(target, ship);
|
||||
}
|
||||
}
|
||||
if (target->ships_alive == 0U) {
|
||||
engine->state.phase = PHASE_FINISHED;
|
||||
engine->state.winner = player;
|
||||
accepted.finished = true;
|
||||
}
|
||||
} else {
|
||||
*cell = CELL_MISS;
|
||||
++statistics->misses;
|
||||
engine->state.current_player ^= 1U;
|
||||
}
|
||||
++engine->state.version;
|
||||
if (result != NULL) *result = accepted;
|
||||
return GAME_RESULT_OK;
|
||||
}
|
||||
Reference in New Issue
Block a user