diff --git a/.circleci/config.yml b/.circleci/config.yml index 1c5cff67..db9dd8da 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 jobs: - suse-qt5-notests: + client-suse-qt5-notests: docker: - image: kdeorg/ci-suse-qt515 steps: @@ -11,11 +11,11 @@ jobs: - checkout - run: name: Cmake - command: cmake client -DQT_MAJOR_VERSION=5 + command: cmake . -DBUILD_SERVER=OFF -DQT_MAJOR_VERSION=5 - run: name: Make - command: make all - suse-qt6-notests: + command: make cavoke_client + client-suse-qt6-notests: docker: - image: kdeorg/ci-suse-qt62 steps: @@ -25,13 +25,31 @@ jobs: command: chmod +x .circleci/suse-qt6-install-karchive.sh && .circleci/suse-qt6-install-karchive.sh - run: name: Cmake - command: cmake client -DQT_MAJOR_VERSION=6 # QT_MAJOR_VERSION=6 by default, so not necessary + command: cmake . -DBUILD_SERVER=OFF -DQT_MAJOR_VERSION=6 # QT_MAJOR_VERSION=6 by default, so not necessary - run: name: Make - command: make all + command: make cavoke_client + server-docker-healthcheck: + docker: + - image: ghcr.io/cavoke-project/cavoke_ci:drogon # TODO: use circleci orbs + auth: + username: $GHCR_USERNAME + password: $GHCR_PASSWORD + steps: + - checkout + - run: + name: Cmake + command: cmake . -DBUILD_CLIENT=OFF + - run: + name: Make + command: make cavoke_server + - run: + name: Simple healthcheck + command: chmod +x .circleci/server-test-health.py && .circleci/server-test-health.py ./server/cavoke_server workflows: client-workflow: jobs: - - suse-qt5-notests - - suse-qt6-notests + - client-suse-qt5-notests + - client-suse-qt6-notests + - server-docker-healthcheck diff --git a/.circleci/server-test-health.py b/.circleci/server-test-health.py new file mode 100644 index 00000000..254efcd8 --- /dev/null +++ b/.circleci/server-test-health.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import atexit +import time + +import requests +import subprocess +import sys + + +def check_eq(expected, actual): + if expected != actual: + print(f'Check failed: expected {repr(expected)}, got {repr(actual)}') + sys.exit(1) + + +def main(): + _, *server_cmd = sys.argv + assert server_cmd, 'Expected usage: ./server-test-health.py ' + + port = 8080 + print(f'Booting server... at {server_cmd}', flush=True) + + server = subprocess.Popen(args=[*server_cmd, '-p', str(port)]) + def kill_server(): + try: + server.wait(timeout=0.1) + except subprocess.TimeoutExpired: + print('Server terminating...', flush=True) + server.kill() + atexit.register(kill_server) + time.sleep(2) # FIXME + print('Checks starting...', flush=True) + + with requests.get(f'http://localhost:{port}/health') as r: + check_eq(200, r.status_code) + check_eq("OK", r.text) + print("Health ok.") + + print('All ok.') + + +if __name__ == '__main__': + main() diff --git a/.postman/schemas/schema.yaml b/.postman/schemas/schema.yaml index beef7086..c7f098eb 100644 --- a/.postman/schemas/schema.yaml +++ b/.postman/schemas/schema.yaml @@ -1,107 +1,226 @@ openapi: 3.0.0 info: - version: '0.0.2' - title: 'cavoke' + version: '0.0.3' + title: 'cavoke' servers: - - url: 'localhost:8080' + - url: 'localhost:8080' paths: - /health: - get: - summary: 'Simple health check' - operationId: health - responses: - '200': - description: 'OK' - content: - text/plain: - schema: - type: string - example: OK - - /games/list: - get: - summary: 'List available games to play' - operationId: listGames - tags: - - game - responses: - '200': - description: 'List of available games as metadata' - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/GameInfo" - default: - description: Unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /games/{game_id}/get_config: - get: - summary: 'Get GameInfo of specific game' - operationId: configGame - tags: - - game - parameters: - - in: path - name: game_id - schema: - $ref: '#/components/schemas/GameId' - required: true - description: 'String id of the game to get' - responses: - '200': - description: 'Config of an existing game' - content: - application/json: - schema: - $ref: "#/components/schemas/GameInfo" - '404': - description: 'No such game' - default: - description: Unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /games/{game_id}/get_client: - get: - summary: 'Download QML client zip of a game' - operationId: downloadClient - tags: - - game - parameters: - - in: path - name: game_id - schema: + /health: + get: + summary: 'Simple health check' + operationId: health + responses: + '200': + description: 'OK' + content: + text/plain: + schema: + type: string + example: OK + + /games/list: + get: + summary: 'List available games to play' + operationId: listGames + responses: + '200': + description: 'List of available games as metadata' + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/GameInfo" + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /games/{game_id}/get_config: + get: + summary: 'Get GameInfo of specific game' + operationId: configGame + parameters: + - in: path + name: game_id + schema: + $ref: '#/components/schemas/GameId' + required: true + description: 'String id of the game to get' + example: tictactoe + responses: + '200': + description: 'Config of an existing game' + content: + application/json: + schema: + $ref: "#/components/schemas/GameInfo" + '404': + description: 'No such game' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /games/{game_id}/get_client: + get: + summary: 'Download QML client zip of a game' + operationId: downloadClient + parameters: + - in: path + name: game_id + schema: + $ref: '#/components/schemas/GameId' + required: true + description: 'String id of the game to get' + example: 'tictactoe' + responses: + '200': + description: 'Client zip file of an existing game' + content: + application/octet-stream: + schema: + type: string + format: binary + '404': + description: 'No such game' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /sessions/create: + post: + summary: 'Create a game from requested game' + operationId: createSession + parameters: + - in: query + name: game_id + schema: + $ref: '#/components/schemas/GameId' + required: true + description: 'String id of the game to get' + example: tictactoe + - in: query + name: user_id + schema: + $ref: '#/components/schemas/UserId' + required: true + description: 'User id' + example: c84d0d6e-547a-4e3a-92ba-5a6ff6c3d51f + responses: + '200': + description: 'Created successfully' + content: + application/json: + schema: + type: object + properties: + game_id: $ref: '#/components/schemas/GameId' - required: true - description: 'String id of the game to get' - example: 'tictactoe' - responses: - '200': - description: 'Client zip file of an existing game' - content: - application/octet-stream: - schema: - type: string - format: binary - '404': - description: 'No such game' - default: - description: Unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + session_id: + $ref: '#/components/schemas/SessionId' + '400': + description: 'Bad request' + '404': + description: 'No game with such game id' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /play/{session_id}/send_move: + post: + summary: 'Send a move from QML game' + operationId: sendMove + parameters: + - in: path + name: session_id + schema: + $ref: '#/components/schemas/SessionId' + required: true + example: 83896dd5-6f03-4805-8cf1-03ce6bd6077f + - in: query + name: user_id + schema: + $ref: '#/components/schemas/UserId' + required: true + description: 'User id' + example: c84d0d6e-547a-4e3a-92ba-5a6ff6c3d51f + requestBody: + description: Game move data + content: + application/json: + schema: + $ref: '#/components/schemas/GameMove' + responses: + '200': + description: 'Move accepted' + + '404': + description: 'No such session / no such user' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /play/{session_id}/get_update: + get: + summary: 'Try get an update from QML game' + operationId: getUpdate + tags: + - play + parameters: + - in: path + name: session_id + schema: + type: string + format: uuid + required: true + description: 'UUID of a game session (room to game pair)' + - in: query + name: user_id + schema: + $ref: '#/components/schemas/UserId' + required: true + description: 'User id' + example: c84d0d6e-547a-4e3a-92ba-5a6ff6c3d51f + - in: query + name: offset + schema: + type: integer + format: int32 + example: 32 + description: 'Only get updates after this id (by default 0, so returns all updates throught the game)' + responses: + '200': + description: 'List updates (in an ascending order of ids)' + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/GameUpdate" + '404': + description: 'No such session / no such user' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: schemas: GameId: type: string + example: tictactoe GameInfo: type: object required: @@ -128,8 +247,41 @@ components: code: type: integer format: int32 + example: 31 message: type: string + example: 'Something went wrong' + GameMove: + type: object + properties: + move: + type: string + description: 'Any information about the move supplied by QML. May be any string (e.g. JSON)' + required: + - user_id + - move + GameUpdate: + type: object + properties: + user_id: + type: string + format: uuid + example: c84d0d6e-547a-4e3a-92ba-5a6ff6c3d51f + update: + type: string + description: "Any information about the update supplied by app's logic. May be any string (e.g. JSON)" + example: '{"action": "x_move", x: 0, y: 2}' + id: + type: integer + format: int32 + example: 1 + required: + - user_id + - update + SessionId: + type: string + format: uuid + example: 83896dd5-6f03-4805-8cf1-03ce6bd6077f securitySchemes: BasicAuth: type: http diff --git a/CMakeLists.txt b/CMakeLists.txt index b2377bb9..657881e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,12 @@ cmake_minimum_required(VERSION 3.10) project(cavoke) -add_subdirectory(client) -add_subdirectory(server) +option(BUILD_SERVER "Enable building server" ON) +option(BUILD_CLIENT "Enable building client" ON) + +if(BUILD_SERVER) + add_subdirectory(server) +endif() +if(BUILD_CLIENT) + add_subdirectory(client) +endif() diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index b625dbcc..bd121ae0 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -1,13 +1,11 @@ cmake_minimum_required(VERSION 3.10) project(cavoke_client) +set(QT_MAJOR_VERSION 6 CACHE STRING "Qt Major Version (e.g. Qt5/Qt6)") + set(CMAKE_CXX_STANDARD 17) set(CMAKE_INCLUDE_CURRENT_DIR ON) -if (NOT QT_MAJOR_VERSION) - set(QT_MAJOR_VERSION 6) -endif () - find_package(Qt${QT_MAJOR_VERSION}Widgets REQUIRED) find_package(Qt${QT_MAJOR_VERSION}Quick REQUIRED) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index b85d4b0d..9c85e105 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -3,12 +3,30 @@ project(cavoke_server) set(CMAKE_CXX_STANDARD 17) -find_package(Drogon REQUIRED) +find_package(Drogon 1.7.3 REQUIRED) -find_package(Boost 1.71 REQUIRED filesystem) +find_package(Boost 1.71 REQUIRED filesystem program_options) include_directories(${Boost_INCLUDE_DIRS}) link_directories(${Boost_LIBRARY_DIRS}) -add_executable(cavoke_server model/games_storage.cpp model/game.cpp controllers/games_controller.cpp main.cpp controllers/health_controller.cpp model/game_state_storage.cpp model/participation_storage.cpp model/game_logic_manager.cpp model/mock_tictactoe/tictactoe.cpp controllers/state_controller.cpp) +# https://github.com/nlohmann/json/releases/tag/v3.9.0 for convenience macros +find_package(nlohmann_json 3.9.0 REQUIRED) + +include_directories(.) + +add_executable(cavoke_server + main.cpp + model/games_storage.cpp + model/game.cpp + controllers/games_controller.cpp + controllers/health_controller.cpp + model/games_storage_config.cpp + model/game_state_storage.cpp + model/participation_storage.cpp + model/game_logic_manager.cpp + model/mock_tictactoe/tictactoe.cpp + controllers/state_controller.cpp + ) target_link_libraries(cavoke_server Drogon::Drogon) target_link_libraries(cavoke_server ${Boost_LIBRARIES} ${CMAKE_DL_LIBS}) +target_link_libraries(cavoke_server nlohmann_json::nlohmann_json) diff --git a/server/controllers/games_controller.cpp b/server/controllers/games_controller.cpp index 689dfd09..75f7ac10 100644 --- a/server/controllers/games_controller.cpp +++ b/server/controllers/games_controller.cpp @@ -1,54 +1,52 @@ #include "games_controller.h" -#include "../model/game.h" - #include #include cavoke::server::controllers::GamesController::GamesController( std::shared_ptr games_storage) - : m_games_storage(std::move(games_storage)) {} + : m_games_storage(std::move(games_storage)) { +} void cavoke::server::controllers::GamesController::games_list( const drogon::HttpRequestPtr &req, std::function &&callback) { - std::vector games = m_games_storage->list_games(); - Json::Value result(Json::arrayValue); - for (const auto &game : games) { - result.append(game.config.to_json()); - } + std::vector games = m_games_storage->list_games(); + std::vector configs(games.size()); + std::transform(games.begin(), games.end(), configs.begin(), + [](const model::Game &g) { return g.config; }); - auto resp = drogon::HttpResponse::newHttpJsonResponse(result); - callback(resp); + auto resp = newNlohmannJsonResponse(configs); + callback(resp); } void cavoke::server::controllers::GamesController::game_config( const drogon::HttpRequestPtr &req, std::function &&callback, const std::string &game_id) { - auto game = m_games_storage->get_game_by_id(game_id); - if (!game.has_value()) { - auto resp = drogon::HttpResponse::newNotFoundResponse(); + auto game = m_games_storage->get_game_by_id(game_id); + if (!game.has_value()) { + auto resp = drogon::HttpResponse::newNotFoundResponse(); + callback(resp); + return; + } + + auto resp = newNlohmannJsonResponse(game->config); callback(resp); - return; - } - - auto resp = drogon::HttpResponse::newHttpJsonResponse(game->config.to_json()); - callback(resp); } void cavoke::server::controllers::GamesController::game_client_file( const drogon::HttpRequestPtr &req, std::function &&callback, const std::string &game_id) { - auto game = m_games_storage->get_game_by_id(game_id); - if (!game.has_value()) { - auto resp = drogon::HttpResponse::newNotFoundResponse(); + auto game = m_games_storage->get_game_by_id(game_id); + if (!game.has_value()) { + auto resp = drogon::HttpResponse::newNotFoundResponse(); + callback(resp); + return; + } + + auto resp = drogon::HttpResponse::newFileResponse( + boost::filesystem::canonical(game->client_file).string(), + game->config.id + ".zip"); callback(resp); - return; - } - - auto resp = drogon::HttpResponse::newFileResponse( - boost::filesystem::canonical(game->client_file).string(), - game->config.id + ".zip"); - callback(resp); } diff --git a/server/controllers/games_controller.h b/server/controllers/games_controller.h index 1d19b9af..cb537624 100644 --- a/server/controllers/games_controller.h +++ b/server/controllers/games_controller.h @@ -1,40 +1,45 @@ #ifndef CAVOKE_SERVER_GAMES_CONTROLLER_H #define CAVOKE_SERVER_GAMES_CONTROLLER_H -#include "../model/games_storage.h" #include +#include "../model/games_storage.h" namespace cavoke::server::controllers { +using json = nlohmann::json; + class GamesController : public drogon::HttpController { - std::shared_ptr m_games_storage; + std::shared_ptr m_games_storage; public: - METHOD_LIST_BEGIN - ADD_METHOD_TO(GamesController::games_list, "/games/list", drogon::Get); - ADD_METHOD_TO(GamesController::game_config, "/games/{game_id}/get_config", - drogon::Get); - ADD_METHOD_TO(GamesController::game_client_file, - "/games/{game_id}/get_client", drogon::Get); - METHOD_LIST_END - - explicit GamesController(std::shared_ptr games_storage); + METHOD_LIST_BEGIN + ADD_METHOD_TO(GamesController::games_list, "/games/list", drogon::Get); + ADD_METHOD_TO(GamesController::game_config, + "/games/{game_id}/get_config", + drogon::Get); + ADD_METHOD_TO(GamesController::game_client_file, + "/games/{game_id}/get_client", + drogon::Get); + METHOD_LIST_END + + explicit GamesController( + std::shared_ptr games_storage); protected: - void - games_list(const drogon::HttpRequestPtr &req, - std::function &&callback); - - void - game_config(const drogon::HttpRequestPtr &req, - std::function &&callback, - const std::string& game_id); - - void game_client_file( - const drogon::HttpRequestPtr &req, - std::function &&callback, - const std::string& game_id); + void games_list( + const drogon::HttpRequestPtr &req, + std::function &&callback); + + void game_config( + const drogon::HttpRequestPtr &req, + std::function &&callback, + const std::string &game_id); + + void game_client_file( + const drogon::HttpRequestPtr &req, + std::function &&callback, + const std::string &game_id); }; -} // namespace cavoke::server::controllers -#endif // CAVOKE_SERVER_GAMES_CONTROLLER_H +} // namespace cavoke::server::controllers +#endif // CAVOKE_SERVER_GAMES_CONTROLLER_H diff --git a/server/controllers/health_controller.cpp b/server/controllers/health_controller.cpp index 1531b4d1..a685cb7c 100644 --- a/server/controllers/health_controller.cpp +++ b/server/controllers/health_controller.cpp @@ -3,8 +3,8 @@ void cavoke::server::controllers::HealthController::health( const drogon::HttpRequestPtr &req, std::function &&callback) { - auto resp = drogon::HttpResponse::newHttpResponse(); - resp->setContentTypeString("text/plain"); - resp->setBody("OK"); - callback(resp); + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setContentTypeCode(drogon::CT_TEXT_PLAIN); + resp->setBody("OK"); + callback(resp); } diff --git a/server/controllers/health_controller.h b/server/controllers/health_controller.h index 3f3a629b..5f957e4d 100644 --- a/server/controllers/health_controller.h +++ b/server/controllers/health_controller.h @@ -6,17 +6,17 @@ namespace cavoke::server::controllers { class HealthController : public drogon::HttpController { - public: - METHOD_LIST_BEGIN - ADD_METHOD_TO(HealthController::health, "/health", drogon::Get); - METHOD_LIST_END + METHOD_LIST_BEGIN + ADD_METHOD_TO(HealthController::health, "/health", drogon::Get); + METHOD_LIST_END protected: - void health(const drogon::HttpRequestPtr &req, - std::function &&callback); + void health( + const drogon::HttpRequestPtr &req, + std::function &&callback); }; -} // namespace cavoke::server::controllers +} // namespace cavoke::server::controllers -#endif // CAVOKE_SERVER_HEALTH_CONTROLLER_H +#endif // CAVOKE_SERVER_HEALTH_CONTROLLER_H diff --git a/server/controllers/state_controller.cpp b/server/controllers/state_controller.cpp index 0c571115..1a8bba50 100644 --- a/server/controllers/state_controller.cpp +++ b/server/controllers/state_controller.cpp @@ -1,90 +1,89 @@ #include "state_controller.h" - #include void cavoke::server::controllers::StateController::send_move( const drogon::HttpRequestPtr &req, std::function &&callback, const std::string &session_id) { + std::optional user_id = + req->getOptionalParameter("user_id"); + + if (!user_id.has_value()) { + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setStatusCode(drogon::HttpStatusCode::k400BadRequest); + callback(resp); + return; + } + + std::optional participant_id = + m_participation_storage->get_participant_id(session_id, + user_id.value()); + + if (!participant_id.has_value()) { + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setStatusCode(drogon::HttpStatusCode::k403Forbidden); + callback(resp); + return; + } + + std::optional current_state = + m_game_state_storage->get_state(session_id); + + if (!current_state.has_value()) { + // new session + current_state = + m_game_logic_manager->send_update("tictactoe", {-1, "", ""}); + } + + current_state = m_game_logic_manager->send_update( + "tictactoe", {participant_id.value(), std::string(req->getBody()), + current_state->global_state}); + + m_game_state_storage->save_state(session_id, current_state.value()); - std::optional user_id = - req->getOptionalParameter("user_id"); - - if (!user_id.has_value()) { - auto resp = drogon::HttpResponse::newHttpResponse(); - resp->setStatusCode(drogon::HttpStatusCode::k400BadRequest); - callback(resp); - return; - } - - std::optional participant_id = - m_participation_storage->get_participant_id(session_id, user_id.value()); - - if (!participant_id.has_value()) { auto resp = drogon::HttpResponse::newHttpResponse(); - resp->setStatusCode(drogon::HttpStatusCode::k403Forbidden); + resp->setStatusCode(drogon::HttpStatusCode::k200OK); callback(resp); - return; - } - - std::optional current_state = - m_game_state_storage->get_state(session_id); - - if (!current_state.has_value()) { - // new session - current_state = - m_game_logic_manager->send_update("tictactoe", {-1, "", ""}); - } - - current_state = m_game_logic_manager->send_update( - "tictactoe", {participant_id.value(), std::string(req->getBody()), - current_state->global_state}); - - m_game_state_storage->save_state(session_id, current_state.value()); - - auto resp = drogon::HttpResponse::newHttpResponse(); - resp->setStatusCode(drogon::HttpStatusCode::k200OK); - callback(resp); } void cavoke::server::controllers::StateController::get_update( const drogon::HttpRequestPtr &req, std::function &&callback, const std::string &session_id) { + std::optional user_id = + req->getOptionalParameter("user_id"); + + if (!user_id.has_value()) { + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setStatusCode(drogon::HttpStatusCode::k400BadRequest); + callback(resp); + return; + } + + std::optional participant_id = + m_participation_storage->get_participant_id(session_id, + user_id.value()); + + if (!participant_id.has_value()) { + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setStatusCode(drogon::HttpStatusCode::k403Forbidden); + callback(resp); + return; + } + + auto state = m_game_state_storage->get_player_state(session_id, + participant_id.value()); + + if (!state.has_value()) { + auto resp = drogon::HttpResponse::newNotFoundResponse(); + callback(resp); + return; + } - std::optional user_id = - req->getOptionalParameter("user_id"); - - if (!user_id.has_value()) { - auto resp = drogon::HttpResponse::newHttpResponse(); - resp->setStatusCode(drogon::HttpStatusCode::k400BadRequest); - callback(resp); - return; - } - - std::optional participant_id = - m_participation_storage->get_participant_id(session_id, user_id.value()); - - if (!participant_id.has_value()) { auto resp = drogon::HttpResponse::newHttpResponse(); - resp->setStatusCode(drogon::HttpStatusCode::k403Forbidden); + resp->setStatusCode(drogon::HttpStatusCode::k200OK); + resp->setBody(state.value()); callback(resp); - return; - } - - auto state = m_game_state_storage->get_player_state(session_id, - participant_id.value()); - - if (!state.has_value()) { - auto resp = drogon::HttpResponse::newNotFoundResponse(); - callback(resp); - return; - } - - auto resp = drogon::HttpResponse::newHttpResponse(); - resp->setStatusCode(drogon::HttpStatusCode::k200OK); - resp->setBody(state.value()); - callback(resp); } cavoke::server::controllers::StateController::StateController( std::shared_ptr mGamesStorage, @@ -94,4 +93,5 @@ cavoke::server::controllers::StateController::StateController( : m_games_storage(std::move(mGamesStorage)), m_game_logic_manager(std::move(mGameLogicManager)), m_game_state_storage(std::move(mGameStateStorage)), - m_participation_storage(std::move(mParticipationStorage)) {} + m_participation_storage(std::move(mParticipationStorage)) { +} diff --git a/server/controllers/state_controller.h b/server/controllers/state_controller.h index d904cc9d..00159914 100644 --- a/server/controllers/state_controller.h +++ b/server/controllers/state_controller.h @@ -1,46 +1,48 @@ #ifndef CAVOKE_STATE_CONTROLLER_H #define CAVOKE_STATE_CONTROLLER_H +#include #include "../model/game_logic_manager.h" #include "../model/games_storage.h" #include "../model/participation_storage.h" -#include namespace cavoke::server::controllers { class StateController : public drogon::HttpController { - std::shared_ptr m_games_storage; - std::shared_ptr m_game_logic_manager; - std::shared_ptr m_game_state_storage; - std::shared_ptr m_participation_storage; + std::shared_ptr m_games_storage; + std::shared_ptr m_game_logic_manager; + std::shared_ptr m_game_state_storage; + std::shared_ptr m_participation_storage; public: - StateController( - std::shared_ptr mGamesStorage, - std::shared_ptr mGameLogicManager, - std::shared_ptr mGameStateStorage, - std::shared_ptr mParticipationStorage); + StateController( + std::shared_ptr mGamesStorage, + std::shared_ptr mGameLogicManager, + std::shared_ptr mGameStateStorage, + std::shared_ptr mParticipationStorage); public: - METHOD_LIST_BEGIN - ADD_METHOD_TO(StateController::send_move, "/play/{session_id}/send_move", - drogon::Post); - ADD_METHOD_TO(StateController::get_update, "/play/{session_id}/get_update", - drogon::Get); - METHOD_LIST_END + METHOD_LIST_BEGIN + ADD_METHOD_TO(StateController::send_move, + "/play/{session_id}/send_move", + drogon::Post); + ADD_METHOD_TO(StateController::get_update, + "/play/{session_id}/get_update", + drogon::Get); + METHOD_LIST_END protected: - void - send_move(const drogon::HttpRequestPtr &req, - std::function &&callback, - const std::string &session_id); - - void - get_update(const drogon::HttpRequestPtr &req, - std::function &&callback, - const std::string &session_id); + void send_move( + const drogon::HttpRequestPtr &req, + std::function &&callback, + const std::string &session_id); + + void get_update( + const drogon::HttpRequestPtr &req, + std::function &&callback, + const std::string &session_id); }; -} // namespace cavoke::server::controllers +} // namespace cavoke::server::controllers -#endif // CAVOKE_STATE_CONTROLLER_H +#endif // CAVOKE_STATE_CONTROLLER_H diff --git a/server/main.cpp b/server/main.cpp index 652ae30b..a0eb03d8 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -1,42 +1,79 @@ +#include +#include #include "controllers/games_controller.h" #include "controllers/state_controller.h" #include "model/game_logic_manager.h" #include "model/games_storage.h" #include "model/participation_storage.h" -#include namespace cavoke::server { -void run() { - // TODO: logging - - // init models - std::cout << "Initialize models..." << std::endl; - auto games_storage = std::make_shared( - model::GamesStorage::GamesStorageConfig{"../../local_server/games"}); - auto game_logic_manager = - std::make_shared(games_storage); - auto game_state_storage = std::make_shared(); - auto participation_storage = std::make_shared(); - - // init controllers - std::cout << "Initialize controllers..." << std::endl; - auto games_controller = - std::make_shared(games_storage); - auto state_controller = std::make_shared( - games_storage, game_logic_manager, game_state_storage, - participation_storage); - - // register controllers - drogon::app().registerController(games_controller); - drogon::app().registerController(state_controller); - - // RUN! - std::cout << "Run server on 127.0.0.1:8080" << std::endl; - drogon::app().addListener("127.0.0.1", 8080).run(); +void run(const std::string &host, + uint16_t port, + const std::string &config_file) { + // TODO: logging + + // init models + std::cout << "Initialize models..." << std::endl; + auto games_storage = std::make_shared( + model::GamesStorageConfig::load(config_file)); + auto game_logic_manager = + std::make_shared(games_storage); + auto game_state_storage = std::make_shared(); + auto participation_storage = + std::make_shared(); + + // init controllers + std::cout << "Initialize controllers..." << std::endl; + auto games_controller = + std::make_shared(games_storage); + auto state_controller = std::make_shared( + games_storage, game_logic_manager, game_state_storage, + participation_storage); + + auto &app = drogon::app(); + + // register controllers + app.registerController(games_controller); + app.registerController(state_controller); + + // start server + std::cout << "Listening at " << host << ":" << port << std::endl; + app.addListener(host, port).run(); } -} // namespace cavoke::server +} // namespace cavoke::server + +namespace po = boost::program_options; + +int main(int argc, char *argv[]) { + po::options_description desc("Allowed options"); + desc.add_options()("help,h", "Print help")( + "config-file,c", po::value(), + "File with game storage configuration")( + "host,ip,a", po::value()->default_value("0.0.0.0"), + "Host on which server is located")( + "port,p", po::value()->default_value(8080), + "TCP/IP port number for connection"); + + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + + if (vm.count("help")) { + std::cout << desc << "\n"; + return 0; + } + + std::string config_file; + if (vm.count("config-file")) { + config_file = vm["config-file"].as(); + } else { + // config_file = std::getenv("CAVOKE_SERVER_CONFIG"); // TODO: think + // and discuss + } + + std::string host = vm.at("host").as(); + uint16_t port = vm.at("port").as(); -int main() { - cavoke::server::run(); - return 0; + cavoke::server::run(host, port, config_file); + return 0; } diff --git a/server/model/game.cpp b/server/model/game.cpp index aa750b65..9dd85430 100644 --- a/server/model/game.cpp +++ b/server/model/game.cpp @@ -1,58 +1,47 @@ #include "game.h" -#include "games_storage.h" #include -#include -#include #include -#include namespace cavoke::server::model { -const std::string Game::CONFIG_FILE = "config.json"; -const std::string Game::CLIENT_FILE = "client.zip"; -const std::string Game::LOGIC_FILE = "logic"; - -Game::Game(boost::filesystem::path directory_) - : directory(std::move(directory_)), logic_file(directory / LOGIC_FILE), - client_file(directory / CLIENT_FILE) { - Json::Value json_config; - read_config_file(directory / CONFIG_FILE, json_config); - config.id = json_config["id"].asString(); - config.display_name = json_config["display_name"].asString(); - config.description = json_config["description"].asString(); - config.players_num = json_config["players_num"].asInt(); +Game::Game(const boost::filesystem::path &directory, + const GamesStorageConfig &game_storage_config) + : directory(directory), + logic_file(directory / LOGIC_FILE), + client_file(directory / CLIENT_FILE), + CLIENT_FILE(game_storage_config.zip_name), + LOGIC_FILE(game_storage_config.logic_name), + CONFIG_FILE(game_storage_config.config_name) { + json j = json::parse(std::ifstream(directory / CONFIG_FILE)); + config = j.get(); } -bool Game::is_game_directory(const boost::filesystem::path &directory) { - Json::Value tmp; - return boost::filesystem::exists(directory) && - boost::filesystem::is_directory(directory) && - boost::filesystem::exists(directory / CLIENT_FILE) && - boost::filesystem::is_regular_file(directory / CLIENT_FILE) && - boost::filesystem::exists(directory / CONFIG_FILE) && - boost::filesystem::is_regular_file(directory / CONFIG_FILE) && - boost::filesystem::exists(directory / LOGIC_FILE) && - boost::filesystem::is_regular_file(directory / LOGIC_FILE) && - read_config_file(directory / CONFIG_FILE, tmp); +/// Checks whether given path is a directory +bool check_is_directory(const boost::filesystem::path &path) { + return exists(path) && is_directory(path); } - -bool Game::read_config_file(const boost::filesystem::path &path, - Json::Value &json_obj) { - std::ifstream file(path); - Json::Reader reader; - bool success = reader.parse(file, json_obj, false); - file.close(); - return success; +/// Checks whether given path exists and is a regular file +bool check_is_regular_file(const boost::filesystem::path &path) { + return exists(path) && is_regular_file(path); +} +/// Checks whether given file is a valid `GameConfig` +bool check_valid_game_config(const boost::filesystem::path &path) { + try { // slow? + json j = json::parse(std::ifstream(path)); + auto conf = j.get(); + conf.validate(); + return true; + } catch (...) { + return false; + } } -Json::Value Game::GameConfig::to_json() const { - Json::Value result; - result["id"] = id; - result["display_name"] = display_name; - result["description"] = description; - result["players_num"] = players_num; - - return result; +bool Game::is_game_directory(const boost::filesystem::path &path, + const GamesStorageConfig &games_storage_config) { + return check_is_directory(path) && + check_is_regular_file(path / games_storage_config.zip_name) && + check_is_regular_file(path / games_storage_config.logic_name) && + check_valid_game_config(path / games_storage_config.config_name); } -} // namespace cavoke::server::model +} // namespace cavoke::server::model diff --git a/server/model/game.h b/server/model/game.h index f22f7033..1809b70c 100644 --- a/server/model/game.h +++ b/server/model/game.h @@ -1,41 +1,52 @@ #ifndef CAVOKE_SERVER_GAME_H #define CAVOKE_SERVER_GAME_H -#include #include +#include +#include #include +#include "games_storage_config.h" namespace cavoke::server::model { -class Game { - static const std::string CONFIG_FILE; - static const std::string CLIENT_FILE; - static const std::string LOGIC_FILE; - - boost::filesystem::path directory; - - struct GameConfig { +struct GameConfig { std::string id; std::string display_name; std::string description; int players_num; - [[nodiscard]] Json::Value to_json() const; - }; + /// Validates config. Throws an exception if invalid + void validate() const { + // TODO: just a sample, `assert` removed in Release + assert(players_num >= 1); + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GameConfig, + id, + display_name, + description, + players_num) + +class Game { + const std::string CONFIG_FILE; + const std::string CLIENT_FILE; + const std::string LOGIC_FILE; - static bool read_config_file(const boost::filesystem::path &path, - Json::Value &json_obj); + boost::filesystem::path directory; public: - explicit Game(boost::filesystem::path directory); - Game() = default; + explicit Game(const boost::filesystem::path &directory, + const GamesStorageConfig &game_storage_config); + Game() = default; - boost::filesystem::path client_file; - GameConfig config; - boost::filesystem::path logic_file; + boost::filesystem::path client_file; + GameConfig config; + boost::filesystem::path logic_file; - static bool is_game_directory(const boost::filesystem::path &path); + static bool is_game_directory( + const boost::filesystem::path &path, + const GamesStorageConfig &games_storage_config); }; -} // namespace cavoke::server::model +} // namespace cavoke::server::model -#endif // CAVOKE_SERVER_GAME_H +#endif // CAVOKE_SERVER_GAME_H diff --git a/server/model/game_logic_manager.cpp b/server/model/game_logic_manager.cpp index 536d86b0..d17fe985 100644 --- a/server/model/game_logic_manager.cpp +++ b/server/model/game_logic_manager.cpp @@ -1,28 +1,28 @@ #include "game_logic_manager.h" -#include "mock_tictactoe/tictactoe.h" - #include +#include "mock_tictactoe/tictactoe.h" namespace cavoke::server::model { GameLogicManager::GameLogicManager(std::shared_ptr games_storage) - : m_games_storage(std::move(games_storage)) {} + : m_games_storage(std::move(games_storage)) { +} -GameStateStorage::GameState -GameLogicManager::send_update(const std::string &game_id, - const GameLogicManager::GameUpdate &update) { - // TODO: invoke actual logic - GameStateStorage::GameState result = GameStateStorage::parse_state( - tictactoe::apply(update.to_json().toStyledString())); - return result; +GameStateStorage::GameState GameLogicManager::send_update( + const std::string &game_id, + const GameLogicManager::GameUpdate &update) { + // TODO: invoke actual logic + GameStateStorage::GameState result = GameStateStorage::parse_state( + tictactoe::apply(update.to_json().toStyledString())); + return result; } Json::Value GameLogicManager::GameUpdate::to_json() const { - Json::Value result; - result["player_id"] = player_id; - result["update"] = update; - result["global_state"] = global_state; + Json::Value result; + result["player_id"] = player_id; + result["update"] = update; + result["global_state"] = global_state; - return result; + return result; } -} // namespace cavoke::server::model +} // namespace cavoke::server::model diff --git a/server/model/game_logic_manager.h b/server/model/game_logic_manager.h index 672940d2..79aa4b81 100644 --- a/server/model/game_logic_manager.h +++ b/server/model/game_logic_manager.h @@ -1,31 +1,31 @@ #ifndef CAVOKE_SERVER_GAME_LOGIC_MANAGER_H #define CAVOKE_SERVER_GAME_LOGIC_MANAGER_H -#include "game_state_storage.h" -#include "games_storage.h" #include #include +#include "game_state_storage.h" +#include "games_storage.h" namespace cavoke::server::model { class GameLogicManager { - std::shared_ptr m_games_storage; + std::shared_ptr m_games_storage; public: - explicit GameLogicManager(std::shared_ptr games_storage); + explicit GameLogicManager(std::shared_ptr games_storage); - struct GameUpdate { - int player_id; - std::string update; - std::string global_state; + struct GameUpdate { + int player_id; + std::string update; + std::string global_state; - [[nodiscard]] Json::Value to_json() const; - }; + [[nodiscard]] Json::Value to_json() const; + }; - GameStateStorage::GameState send_update(const std::string &game_id, - const GameUpdate &update); + GameStateStorage::GameState send_update(const std::string &game_id, + const GameUpdate &update); }; -} // namespace cavoke::server::model +} // namespace cavoke::server::model -#endif // CAVOKE_SERVER_GAME_LOGIC_MANAGER_H +#endif // CAVOKE_SERVER_GAME_LOGIC_MANAGER_H diff --git a/server/model/game_state_storage.cpp b/server/model/game_state_storage.cpp index d105cb9b..bfa64c94 100644 --- a/server/model/game_state_storage.cpp +++ b/server/model/game_state_storage.cpp @@ -1,5 +1,4 @@ #include "game_state_storage.h" - #include #include @@ -7,46 +6,48 @@ namespace cavoke::server::model { void GameStateStorage::save_state(const std::string &session_id, GameStateStorage::GameState new_state) { - m_states[session_id] = std::move(new_state); + m_states[session_id] = std::move(new_state); } -std::optional -GameStateStorage::get_state(const std::string &session_id) { - if (m_states.count(session_id) == 0) { - return {}; - } - return m_states[session_id]; +std::optional GameStateStorage::get_state( + const std::string &session_id) { + if (m_states.count(session_id) == 0) { + return {}; + } + return m_states[session_id]; } -GameStateStorage::GameState -GameStateStorage::parse_state(const std::string &s) { - GameState state; - Json::Value json_state; - Json::Reader reader; - reader.parse(s, json_state, false); - state.is_terminal = json_state["is_terminal"].asBool(); - state.global_state = json_state["global_state"].asString(); - for (Json::Value::ArrayIndex i = 0; i != json_state["players_state"].size(); - i++) { - state.players_state.push_back(json_state["players_state"][i].asString()); - } - for (Json::Value::ArrayIndex i = 0; i != json_state["winners"].size(); i++) { - state.winners.push_back(json_state["winners"][i].asInt()); - } - return state; +GameStateStorage::GameState GameStateStorage::parse_state( + const std::string &s) { + GameState state; + Json::Value json_state; + Json::Reader reader; + reader.parse(s, json_state, false); + state.is_terminal = json_state["is_terminal"].asBool(); + state.global_state = json_state["global_state"].asString(); + for (Json::Value::ArrayIndex i = 0; i != json_state["players_state"].size(); + i++) { + state.players_state.push_back( + json_state["players_state"][i].asString()); + } + for (Json::Value::ArrayIndex i = 0; i != json_state["winners"].size(); + i++) { + state.winners.push_back(json_state["winners"][i].asInt()); + } + return state; } -std::optional -GameStateStorage::get_player_state(const std::string &session_id, - int player_id) { - auto state = get_state(session_id); - if (!state.has_value()) { - return {}; - } - if (player_id >= state->players_state.size()) { - return {}; - } - return state->players_state[player_id]; +std::optional GameStateStorage::get_player_state( + const std::string &session_id, + int player_id) { + auto state = get_state(session_id); + if (!state.has_value()) { + return {}; + } + if (player_id >= state->players_state.size()) { + return {}; + } + return state->players_state[player_id]; } -} // namespace cavoke::server::model +} // namespace cavoke::server::model diff --git a/server/model/game_state_storage.h b/server/model/game_state_storage.h index 2125a94f..4a14c25b 100644 --- a/server/model/game_state_storage.h +++ b/server/model/game_state_storage.h @@ -8,28 +8,28 @@ namespace cavoke::server::model { -class GameStateStorage { // TODO: thread safety +class GameStateStorage { // TODO: thread safety public: - struct GameState { - bool is_terminal; - std::string global_state; - std::vector players_state; - std::vector winners; - }; + struct GameState { + bool is_terminal; + std::string global_state; + std::vector players_state; + std::vector winners; + }; - static GameState parse_state(const std::string &s); + static GameState parse_state(const std::string &s); - void save_state(const std::string &session_id, GameState new_state); + void save_state(const std::string &session_id, GameState new_state); - std::optional get_state(const std::string &session_id); + std::optional get_state(const std::string &session_id); - std::optional get_player_state(const std::string &session_id, - int player_id); + std::optional get_player_state(const std::string &session_id, + int player_id); private: - std::map m_states; + std::map m_states; }; -} // namespace cavoke::server::model +} // namespace cavoke::server::model -#endif // CAVOKE_SERVER_GAME_STATE_STORAGE_H +#endif // CAVOKE_SERVER_GAME_STATE_STORAGE_H diff --git a/server/model/games_storage.cpp b/server/model/games_storage.cpp index 83bb434a..a9430bc0 100644 --- a/server/model/games_storage.cpp +++ b/server/model/games_storage.cpp @@ -7,57 +7,58 @@ namespace cavoke::server::model { -GamesStorage::GamesStorage(GamesStorage::GamesStorageConfig config_) - : m_config(std::move(config_)) { - - if (!boost::filesystem::exists(m_config.games_directory)) { - bool success = - boost::filesystem::create_directories(m_config.games_directory); - if (!success) { - throw std::invalid_argument("Cannot create games directory " + - m_config.games_directory.string() + "\n"); +GamesStorage::GamesStorage(GamesStorageConfig config) + : m_config(std::move(config)) { + if (!boost::filesystem::exists(m_config.games_directory)) { + bool success = + boost::filesystem::create_directories(m_config.games_directory); + if (!success) { + throw std::invalid_argument("Cannot create games directory " + + m_config.games_directory.string() + + "\n"); + } } - } - if (!boost::filesystem::is_directory(m_config.games_directory)) { - throw std::invalid_argument("Invalid games directory " + - m_config.games_directory.string() + "\n"); - } + if (!boost::filesystem::is_directory(m_config.games_directory)) { + throw std::invalid_argument("Invalid games directory " + + m_config.games_directory.string() + "\n"); + } - update(); + update(); } std::vector GamesStorage::list_games() { - update(); + update(); - std::vector result; - for (auto e : m_games) { - result.emplace_back(e.second); - } - return result; + std::vector result; + for (auto e : m_games) { + result.emplace_back(e.second); + } + return result; } void GamesStorage::update() { - m_games.clear(); - for (auto &entry : boost::make_iterator_range( - boost::filesystem::directory_iterator(m_config.games_directory), - {})) { - if (!Game::is_game_directory(entry)) { - // TODO: logging - std::cerr << entry << " entity from games directory is not a valid game" - << std::endl; - } else { - Game game(entry); - m_games.emplace(game.config.id, game); + m_games.clear(); + for (auto &entry : boost::make_iterator_range( + boost::filesystem::directory_iterator(m_config.games_directory), + {})) { + if (!Game::is_game_directory(entry, m_config)) { + // TODO: logging + std::cerr << entry + << " entity from games directory is not a valid game" + << std::endl; + } else { + Game game(entry, m_config); + m_games.emplace(game.config.id, game); + } } - } } -std::optional GamesStorage::get_game_by_id(const std::string& game_id) { - if (m_games.count(game_id) > 0) { - return m_games[game_id]; - } - return {}; +std::optional GamesStorage::get_game_by_id(const std::string &game_id) { + if (m_games.count(game_id) > 0) { + return m_games[game_id]; + } + return {}; } -} // namespace cavoke::server::model +} // namespace cavoke::server::model diff --git a/server/model/games_storage.h b/server/model/games_storage.h index fefa1304..2261318e 100644 --- a/server/model/games_storage.h +++ b/server/model/games_storage.h @@ -1,33 +1,29 @@ #ifndef CAVOKE_GAMES_STORAGE_H #define CAVOKE_GAMES_STORAGE_H -#include "game.h" -#include #include +#include #include - +#include "games_storage_config.h" +#include "game.h" namespace cavoke::server::model { class GamesStorage { - // TODO: thread-safety + // TODO: thread-safety public: - struct GamesStorageConfig { - boost::filesystem::path games_directory; - }; - - explicit GamesStorage(GamesStorageConfig config); + explicit GamesStorage(GamesStorageConfig config); - void update(); + void update(); - std::vector list_games(); + std::vector list_games(); - std::optional get_game_by_id(const std::string &game_id); + std::optional get_game_by_id(const std::string &game_id); private: - GamesStorageConfig m_config; - std::map m_games; + GamesStorageConfig m_config; + std::map m_games; }; -} // namespace cavoke::server::model +} // namespace cavoke::server::model -#endif // CAVOKE_GAMES_STORAGE_H +#endif // CAVOKE_GAMES_STORAGE_H diff --git a/server/model/games_storage_config.cpp b/server/model/games_storage_config.cpp new file mode 100644 index 00000000..46a48c11 --- /dev/null +++ b/server/model/games_storage_config.cpp @@ -0,0 +1,26 @@ +#include "games_storage_config.h" +#include +#include + +namespace cavoke::server::model { + +GamesStorageConfig GamesStorageConfig::load(const std::string &config_file) { + if (config_file.empty()) { + // FIXME: rewrite this mess... + std::cerr << "No games storage config specified. Using a default one. " + "Please use `-c` option.\n"; + return GamesStorageConfig{"../../local_server/games", "logic", + "client.zip", "config.json"}; + } + if (!boost::filesystem::exists(config_file) || + !boost::filesystem::is_regular_file(config_file)) { // FIXME + throw std::invalid_argument( + "No such game storage configuration file: " + config_file); + } + + std::ifstream file(config_file); + json j = json::parse(file); + return j.get(); +} + +} // namespace cavoke::server::model \ No newline at end of file diff --git a/server/model/games_storage_config.h b/server/model/games_storage_config.h new file mode 100644 index 00000000..2a2bc1fa --- /dev/null +++ b/server/model/games_storage_config.h @@ -0,0 +1,24 @@ +#ifndef CAVOKE_GAMES_STORAGE_CONFIG_H +#define CAVOKE_GAMES_STORAGE_CONFIG_H + +#include +#include +#include "utils.h" +namespace cavoke::server::model { +using json = nlohmann::json; + +struct GamesStorageConfig { + boost::filesystem::path games_directory; + std::string logic_name; + std::string zip_name; + std::string config_name; + + static GamesStorageConfig load(const std::string &config_file); +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GamesStorageConfig, + games_directory, + logic_name, + zip_name, + config_name) +} // namespace cavoke::server::model +#endif // CAVOKE_GAMES_STORAGE_CONFIG_H diff --git a/server/model/mock_tictactoe/tictactoe.cpp b/server/model/mock_tictactoe/tictactoe.cpp index 85acb4ba..d46fbca7 100644 --- a/server/model/mock_tictactoe/tictactoe.cpp +++ b/server/model/mock_tictactoe/tictactoe.cpp @@ -1,9 +1,9 @@ #include "tictactoe.h" -#include #include +#include +#include #include #include -#include namespace tictactoe { @@ -11,110 +11,113 @@ std::mt19937 gen; std::uniform_real_distribution<> dis; bool winner(std::string board) { - for (int i = 0; i < 3; ++i) { - if (board[i] != ' ' && board[i] == board[i + 3] && board[i] == board[i + 6]) - return true; - - if (board[i * 3] != ' ' && board[i * 3] == board[i * 3 + 1] && - board[i * 3] == board[i * 3 + 2]) - return true; - } + for (int i = 0; i < 3; ++i) { + if (board[i] != ' ' && board[i] == board[i + 3] && + board[i] == board[i + 6]) + return true; + + if (board[i * 3] != ' ' && board[i * 3] == board[i * 3 + 1] && + board[i * 3] == board[i * 3 + 2]) + return true; + } - if (board[0] != ' ' && board[0] == board[4] && board[0] == board[8]) - return true; + if (board[0] != ' ' && board[0] == board[4] && board[0] == board[8]) + return true; - if (board[2] != ' ' && board[2] == board[4] && board[2] == board[6]) - return true; + if (board[2] != ' ' && board[2] == board[4] && board[2] == board[6]) + return true; - return false; + return false; } bool makeMove(int pos, char player, std::string &board) { - board[pos] = player; - if (winner(board)) { - // gameFinished(player + " wins") - return true; - } - return false; + board[pos] = player; + if (winner(board)) { + // gameFinished(player + " wins") + return true; + } + return false; } void restartGame(std::string &board) { - for (int i = 0; i < 9; ++i) { - board[i] = ' '; - } + for (int i = 0; i < 9; ++i) { + board[i] = ' '; + } } -bool canPlayAtPos(int pos, std::string &board) { return board[pos] == ' '; } +bool canPlayAtPos(int pos, std::string &board) { + return board[pos] == ' '; +} void randomAI(std::string &board) { - std::vector unfilledPosns; + std::vector unfilledPosns; + + for (int i = 0; i < 9; ++i) { + if (canPlayAtPos(i, board)) { + unfilledPosns.emplace_back(i); + } + } - for (int i = 0; i < 9; ++i) { - if (canPlayAtPos(i, board)) { - unfilledPosns.emplace_back(i); + if (unfilledPosns.empty()) { + restartGame(board); + } else { + int choice = unfilledPosns[std::floor(dis(gen) * unfilledPosns.size())]; + makeMove(choice, 'O', board); } - } - - if (unfilledPosns.empty()) { - restartGame(board); - } else { - int choice = unfilledPosns[std::floor(dis(gen) * unfilledPosns.size())]; - makeMove(choice, 'O', board); - } } std::string apply(const std::string &request) { - // TODO: logging - std::cout << "RECEIVED MOVE " << request << std::endl; - Json::Reader reader; - Json::Value json_request; - reader.parse(request, json_request, false); - std::string board = json_request["global_state"].asString(); - std::string update = json_request["update"].asString(); - int player_id = json_request["player_id"].asInt(); - - std::string new_global_state; - std::string new_player_state; - - std::string message = "..."; - - // player_id == -1 -> start new game - if (player_id == -1) { - board = std::string(9, ' '); - } else { - std::stringstream to_split(update); - char action; - to_split >> action; - if (action != 'D') { - int pos; - to_split >> pos; - if (!(pos >= 0 && pos < 9 && canPlayAtPos(pos, board))) { - message= "Invalid action"; - } else { - makeMove(pos, 'X', board); - if (winner(board)) { - message = "X wins"; - restartGame(board); - } else { - randomAI(board); - if (winner(board)) { - message = "O wins"; - restartGame(board); - } + // TODO: logging + std::cout << "RECEIVED MOVE " << request << std::endl; + Json::Reader reader; + Json::Value json_request; + reader.parse(request, json_request, false); + std::string board = json_request["global_state"].asString(); + std::string update = json_request["update"].asString(); + int player_id = json_request["player_id"].asInt(); + + std::string new_global_state; + std::string new_player_state; + + std::string message = "..."; + + // player_id == -1 -> start new game + if (player_id == -1) { + board = std::string(9, ' '); + } else { + std::stringstream to_split(update); + char action; + to_split >> action; + if (action != 'D') { + int pos; + to_split >> pos; + if (!(pos >= 0 && pos < 9 && canPlayAtPos(pos, board))) { + message = "Invalid action"; + } else { + makeMove(pos, 'X', board); + if (winner(board)) { + message = "X wins"; + restartGame(board); + } else { + randomAI(board); + if (winner(board)) { + message = "O wins"; + restartGame(board); + } + } + } } - } } - } - std::string new_state = message + "\n" + board; + std::string new_state = message + "\n" + board; - Json::Value json_result; - json_result["is_terminal"] = false; - json_result["global_state"] = board; - json_result["players_state"].append(board); - json_result["winners"] = Json::arrayValue; + Json::Value json_result; + json_result["is_terminal"] = false; + json_result["global_state"] = board; + json_result["players_state"].append(board); + json_result["winners"] = Json::arrayValue; - return json_result.toStyledString(); + return json_result.toStyledString(); } -} // namespace tictactoe +} // namespace tictactoe diff --git a/server/model/mock_tictactoe/tictactoe.h b/server/model/mock_tictactoe/tictactoe.h index cbc1d194..5445400f 100644 --- a/server/model/mock_tictactoe/tictactoe.h +++ b/server/model/mock_tictactoe/tictactoe.h @@ -9,4 +9,4 @@ std::string apply(const std::string &request); } -#endif // CAVOKE_TICTACTOE_H +#endif // CAVOKE_TICTACTOE_H diff --git a/server/model/participation_storage.cpp b/server/model/participation_storage.cpp index 60548675..b9557d56 100644 --- a/server/model/participation_storage.cpp +++ b/server/model/participation_storage.cpp @@ -1,10 +1,12 @@ #include "participation_storage.h" -cavoke::server::model::ParticipationStorage::ParticipationStorage() {} +cavoke::server::model::ParticipationStorage::ParticipationStorage() { +} std::optional cavoke::server::model::ParticipationStorage::get_participant_id( - const std::string &session_id, const std::string &user_id) { - // TODO - return 0; + const std::string &session_id, + const std::string &user_id) { + // TODO + return 0; } diff --git a/server/model/participation_storage.h b/server/model/participation_storage.h index 02041902..24e9fe38 100644 --- a/server/model/participation_storage.h +++ b/server/model/participation_storage.h @@ -6,16 +6,15 @@ namespace cavoke::server::model { class ParticipationStorage { - // TODO: thread-safety + // TODO: thread-safety public: - ParticipationStorage(); - - std::optional get_participant_id(const std::string& session_id, - const std::string& user_id); + ParticipationStorage(); + std::optional get_participant_id(const std::string &session_id, + const std::string &user_id); }; -} // namespace cavoke::server::model +} // namespace cavoke::server::model -#endif // CAVOKE_SERVER_PARTICIPATION_STORAGE_H +#endif // CAVOKE_SERVER_PARTICIPATION_STORAGE_H diff --git a/server/utils.h b/server/utils.h new file mode 100644 index 00000000..e7686916 --- /dev/null +++ b/server/utils.h @@ -0,0 +1,40 @@ +#ifndef CAVOKE_UTILS_H +#define CAVOKE_UTILS_H + +#include +#include +#include + +namespace nlohmann { +/// nlohmann::json serializer for `boost::filesystem::path` +template <> +struct adl_serializer { + static void to_json(json &j, const boost::filesystem::path &value) { + j = value.string(); + } + + static void from_json(const json &j, boost::filesystem::path &value) { + value = j.get(); + } +}; +} // namespace nlohmann + +namespace cavoke::server::controllers { + +/** + * Creates a drogon response from serializable object. + * + * Preferably pass the object itself, not the json object, + * because `nlohmann::json{configs}` adds extra square brackets to objects + * see https://github.com/cavoke-project/cavoke/issues/22. Avoid curly braces. + */ +inline drogon::HttpResponsePtr newNlohmannJsonResponse( + const nlohmann::json &obj) { + auto res = drogon::HttpResponse::newHttpResponse(); + res->setContentTypeCode(drogon::CT_APPLICATION_JSON); + res->setBody(obj.dump()); + return res; +} +} // namespace cavoke::server::controllers + +#endif // CAVOKE_UTILS_H