Stockfighter — это игра-симулятор. Каждый уровень (с возрастающей сложностью) управляется через браузерный промокательный пользовательский интерфейс, хотя после первого или двух уровней единственный способ победить — написать клиент для REST API симулятора. (Кроме того, компания, издающая игру, имеет для этого интересную причину.)

Реальность такова, что я не могу очень хорошо программировать веб: у меня не было особой потребности. Я могу свернуть адрес просто отлично, но редко нуждаюсь в хорошо структурированном виде. Веб-задержка требует асинхронной обработки вызовов, что является еще одним навыком, который я пытался улучшить, учитывая новые встроенные функции C++11. Предстоящие работы в офисе потребуют этих навыков, поэтому Stockfighter — это хорошая возможность одновременно заниматься архитектурой и учиться.

Все приложение, от супа до орехов, написано на C++11. Я пользуюсь некоторыми сторонними инструментами (читай: Excel) для создания базовых диаграмм, но даже в этом случае данные поступают из журналов, которые мне нужно было построить.

Я использовал некоторые сторонние библиотеки, чтобы немного упростить низкоуровневую работу:

  • libcurl (для деталей REST API)
  • буст (незаменимые безделушки)
  • websocketpp (детали веб-сокета)
  • json11 (для кругового обмена JSON)

Websocketpp использует библиотеку asio от Boost, которую было достаточно легко интегрировать, учитывая, что я уже собирался использовать Boost. Сетевые библиотеки также используют OpenSSL для шифрования связи, хотя я не взаимодействовал с ним напрямую.

Я не буду описывать каждую кровавую строчку C++. Я также не буду вдаваться в подробности, связанные с прохождением того или иного уровня Stockfighter. Я надеюсь, что то, что я создал, способно решить любую из них (с учетом некоторых относительно незначительных изменений). Скорее, меня больше всего интересует выявление конкретных областей, в которых, как мне кажется, я узнал что-то новое или создал что-то интересное.

Цели

При создании клиента у меня не было много целей. Очевидно, что победа на различных уровнях была важна. Но я также был заинтересован в том, чтобы приложение было стабильным, удобным в сопровождении и расширяемым. Это означает:

  • Нет петель для блокировки. Я сделал что-то не так, если процессор загружается до 100%, ожидая, что что-то произойдет.
  • Представление. Если реакция на событие биржевой симуляции займет слишком много времени, мое окно возможностей будет упущено.
  • Безопасность резьбы. Если приложение наступает на собственные ноги, оно не продвинется далеко.

Итак, с чего начать?

Как насчет начала:

int main(int argc, char** argv) try {
    std::string settings_path(argc > 1 ? argv[1] : "");

    if (!config::init(binary_path, settings_path)) {
        // usage, error, etc

        return 1;
    }

    log_t log(config::derivative_file(".log"), true, false);
    task_queue_t queue{6};
    recur::engine_t recur{queue};
    game_t game(log, recur, queue);

    std::thread([&](){
        console(log, recur, queue, game);
    }).detach();

    recur.insert(std::chrono::minutes(1), [&]() {
        keepalive(log, recur);
    });

    log("MAIN") << "Startup";

    queue.push([&](){
        game.start();
    });

    recur.run();

    return 0;
} catch (const std::exception& error) {
    std::cerr << "Fatal error : " << error.what() << '\n';

    return 1;
} catch (...) {
    std::cerr << "Fatal error : Unknown" << '\n';

    return 1;
}

Заряжено, да? Давайте сломаем это.

Файл настроек

Приложение принимает единственный параметр: путь к файлу настроек. Файл представляет собой сериализованный объект JSON с одним значением — моим ключом API Stockfighter. (Я намеревался немного больше использовать файл настроек, но причина так и не материализовалась.)

Как оказалось, наиболее важной функцией файла настроек является предоставление домашнего каталога для файлов журнала.

Журнал

Для многих уровней у меня было несколько журналов, куда я выбрасывал большое количество данных. Вызов config::derivative_file начинается с расположения файла настроек и возвращает родственный файл в виде boost::filesystem::path. Я построил журналы так, чтобы они вели себя так же, как iostreams, но также включили поддержку потоков, временные метки и т. д.

Этот журнал, в частности, является основным журналом. Думайте об этом как о std::cout.

Очередь задач

Как я упоминал ранее, асинхронная обработка событий необходима в современных приложениях. Это удваивается при работе с ресурсами с высокой задержкой (например, веб-API). Очередь задач позволяет мне настраивать задания в нескольких потоках для одновременной обработки. По мере того, как информация поступает от клиента в Stockfighter и уходит от него, возможность своевременно реагировать на все это будет иметь решающее значение. Очередь задач позволяет мне сделать это, но при этом я должен учитывать асинхронные проблемы. Вызов принят.

Рекуррентный двигатель

Многие вещи должны происходить на постоянной основе и через определенные промежутки времени. Примеры этого включают опрос событий веб-сокета или подтверждение того, что серверы Stockfighter работают (тактовые импульсы). Механизм повторяющихся событий отслеживает эти задания и при необходимости вставляет их в очередь задач. После завершения задания оно переносится в «очередь ожидания» для повторного вызова в более позднее время.

Даже когда очередь задач пуста, рекуррентный движок всегда бдителен, решая, какое задание (если оно есть) должно быть запущено следующим и когда. Все, что делает приложение при взаимодействии со Stockfighter, уходит своими корнями в рекуррентный движок.

Аналогичным образом, демонтаж приложения начинается с остановки рекуррентного движка и представляет собой структуру, используемую для определения того, "готово" ли приложение. Когда этот движок останавливается, все приложение рушится. Очередь задач пустеет. Веб-сокеты не работают. Сердцебиение умирает.

Интерфейс игры

Структура игры является инкапсуляцией всей системы Stockfighter. Вы можете видеть, что он получает журнал, рекуррент и очередь задач по мере создания. И не зря, так как почти все, что происходит в приложении, находится в игровом блоке. (Игровые подсистемы разбиты на несколько других файлов, все во имя инкапсуляции и ясности.)

Консоль

Большая часть намерений приложения состоит в том, чтобы обеспечить бесперебойный интерфейс со Stockfighter. Тем не менее, бывают случаи, когда необходимо небольшое ручное вмешательство. Консоль — это отдельная единица приложения, которая асинхронно обрабатывает std::cout и std::cin. В зависимости от пользовательского ввода различные задачи помещаются в очередь задач для выполнения. В итоге я написал около полудюжины команд для консоли. Большинство из них носило исследовательский характер и мало что делало для непосредственного контроля поведения клиента на уровне.

Консоль живет в своем собственном потоке и использует рекуррентный движок только для того, чтобы увидеть, завершился ли он. Как только он завершится, мне не нужно напрямую с ним общаться, и нить можно безопасно отсоединить.

Keepalive

keepalive мало что делает. Он установлен в рекуррентном двигателе, чтобы стрелять ежеминутно. У Stockfighter есть API для проверки работы серверов, и если они не работают, периодический движок прекращает работу:

void keepalive(log_t& log, recur::engine_t& recur) {
    // If the API dies, so should we.
    if (!stock::heartbeat()) {
        log(“MAIN”) << “Heartbeat failure”;
        recur.terminate();
    }
}

Это одно из первых применений механизма ведения журнала. Я хотел, чтобы он вел себя как iostream, насколько это возможно, поэтому я мог обращаться с ним как с std::cout. Я не хотел думать об его использовании, я просто хотел, чтобы он работал. Я могу более подробно остановиться на механизме ведения журнала в следующем посте.

Начало игры

Со всеми настройками пришло время что-то делать! Игра начинается с… ну… game.start(). Я помещаю его в очередь задач, потому что хочу, чтобы он выполнялся где-то еще — мой рекуррентный движок должен быть занят повторением.

Запуск игрового движка выполняет несколько важных действий, таких как запуск экземпляра игры, установка необходимых веб-сокетов, настройка еще одного пульса и регистрация некоторых основных сведений об уровне. Что происходит после этого — история для другого поста.

Конец главного

Наконец, рекуррентный движок — это последняя структура, упомянутая в main: вызов подпрограммы движка run переводит основной поток в цикл ожидания до планирования следующего повторяющегося задания, или бросить.