Skip to content

LOG: CGOptimum

Romutchio edited this page Jun 10, 2019 · 1 revision

Проблема

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

Цель

  1. В рамках командного проекта сделать бота для телеграмма, который позволит тренироваться в оценке сложности алгоритмов в свободное время прямо на своем телефоне. Для этого разработать механику, плавного увеличения сложности задач, с учетом успеваемости пользователя. Задания должны быть каждый раз уникальными и генерироваться на лету, чтобы исключить возможность запоминания уже увиденных вариантов.
  2. Обобщить бота так, чтобы его можно было использовать для других тем, отличных от оценки сложности алгоритмов.
  3. Обобщить код так, чтобы незначительными доработками можно было сделать его доступным через другие платформы для чат-ботов.

Архитектура решения

Решение построено из микросервисов, взаимодействующих между собой по протоколу HTTP.
Архитекрута

Core

Бизнес-логика викторины, построенная согласно принципам DDD.

Task API

Внешнее REST API для работы с задачами и уровнями. Построено на фреймворке ASP.NET Core.

Quiz API

Внешнее REST API для работы с викториной и пользователями. Построено на фреймворке ASP.NET Core.

Quiz Database

Интерфейс для работы с MongoDB: заданиями, пользователями и другой статистикой.

Telegram Bot

Построенный на ASP.NET Core webhook-bot для Telegram.

Telegram Users Database

Интерфейс для работы с MongoDB: состояниями пользователей и их данными для аутентификации в Task API.

Editor

Веб-сервис для редактирования и дизайна уровней викторины. Фронтенд написан на React JS, бекэнд - на ASP.NET Core. 

Постановка задачи

  1. Создать удобное API для викторины
  2. Реализовать логику викторины:
    • Поиск задания и прогресса пользователя в базе данных
    • Выбор задания из предложенных вариантов
    • Обновление базы данных с учетом новых изменений
  3. Защитить логику от действий, недоступных в данный момент конкретному пользователю

Решение

Классы с данными

Для того, чтобы не отдавать пользователю всю информацию о теме, уровне или задаче (например секретные данные, такие как правильный ответ на вопрос) и некоторые данные отдавать порционно (например подсказки) были созданы отдельные сущности, названные Info: 

public class TopicInfo
{
    public Guid Id { get; }
    public string Name { get; }
}

public class LevelInfo
{
    public Guid Id { get; }
    public string Description { get; }
}

public class TaskInfo
{
    public string Question { get; }
    public string[] Answers { get; }
    public bool HasHints { get; }
    public string Text { get; }
}

public class HintInfo
{
    public string HintText { get; }
    public bool HasNext { get; }
}

public class LevelProgressInfo
{
    public int TasksCount { get; }
    public int TasksSolved { get; }
}

Result<T, TExp>

Интерфейс сервиса викторины использует эти классы внутри возвращаемого значения, которое представляет из себя объект класса-обертки:

public class Result<T, TExp>
{
    public T Value { get; }
    public TExp Error { get; }
    public bool IsSuccessful { get; }
}

Данное решение позволяет не выбрасывать исключения, а передавать их как результат работы метода, снизив вероятность необработанного исключения и ускорив работу программы

Аргументы

Данные о теме/уровне/пользователе передаются в виде их ID для того, чтобы получать обновленную информацию из базы данных (или другой реализации хранилища данных) и унифицировать работу со всеми сущностями

Интерфейс

public interface IQuizService
{
    Result<IEnumerable<TopicInfo>, Exception> GetTopicsInfo();
    Result<IEnumerable<LevelInfo>, Exception> GetLevels(Guid topicId);
    Result<IEnumerable<LevelInfo>, Exception> GetAvailableLevels(Guid userId, Guid topicId);
    Result<LevelProgressInfo, Exception> GetProgress(Guid userId, Guid topicId, Guid levelId);
    Result<TaskInfo, Exception> GetTask(Guid userId, Guid topicId, Guid levelId);
    Result<TaskInfo, Exception> GetNextTask(Guid userId);
    Result<bool, Exception> CheckAnswer(Guid userId, string answer);
    Result<HintInfo, Exception> GetHint(Guid userId);
}

GetTopicsInfo

Получение списка тем

GetLevels

Получение списка всех уровней, которые есть в теме Может вернуть ArgumentException, если передан несуществующий ID темы

GetAvailableLevels

Получение списка уровней, доступных пользователю, которые есть в теме Может вернуть ArgumentException, если передан несуществующий ID темы

GetProgress

Получение прогресса пользователя в уровне Может вернуть ArgumentException, если передан несуществующий ID темы или уровня, AccessDeniedException, если у пользователя еще нет доступа к этому уровню

GetTask

Получение задачи из темы и уровня Может вернуть ArgumentException, если передан несуществующий ID темы или уровня, AccessDeniedException, если у пользователя еще нет доступа к этому уровню

GetNextTask

Получение следующей задачи из текущих темы и уровня пользователя Может вернуть AccessDeniedException, если у пользователь еще не начал ни одного уровня

CheckAnswer

Проверка правильности ответа на текущую задачу Может вернуть AccessDeniedException, если у пользователь еще не начал задачу

GetHint

Получение подсказки для текущей задачи Может вернуть AccessDeniedException, если у пользователь еще не начал задачу, OutOfHintsException, если подсказки кончились

Документация

Для каждого метода написана XML документация, для удобного использования API другими членами команды 

Реализация

Для реализации логики был создан следующий класс:

public class QuizService : IQuizService
{
    private readonly ITaskGeneratorSelector generatorSelector;
    private readonly Random random;
    private readonly ITaskRepository taskRepository;
    private readonly IUserRepository userRepository;
}

taskRepository и userRepository

Объекты для работы с репозиторием задач и пользователей соответственно

random

Объект для рандомизированного выбора генератора и создания задачи

generatorSelector

Объект для выбора генератора из списка, он нем подробно написано ниже

Принцип работы

  1. Проверка правильности переданных аргументов (ID)
  2. Обновление прогресса пользователя в случае изменений в базе данных заданий
  3. Проверка доступности данного метода для пользователя
  4. Выполнение логики метода
  5. Обновление прогресса пользователя
  6. Возвращение результата

ITaskGeneratorSelector

Для выбора генератора из списка, было решено использовать шаблон “Стратегия”. Интерфейс ITaskGeneratorSelector получает список генераторов и текущий прогресс пользователя, после чего отдает конкретный генератор из списка. 

public interface ITaskGeneratorSelector
{
    Result<TaskGenerator, Exception> SelectGenerator(
        IEnumerable<TaskGenerator> generators,
        Dictionary<Guid, int> streaks);
}

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

public class ProgressTaskGeneratorSelector : ITaskGeneratorSelector
{
    private readonly Random random;
    private readonly ITaskGeneratorSelector alternativeSelector;

    public Result<TaskGenerator, Exception> SelectGenerator(
        IEnumerable<TaskGenerator> generators,
        Dictionary<Guid, int> streaks)
    {
        var generatorsArray = generators as TaskGenerator[] ?? generators.ToArray();
        var variants = new List<TaskGenerator>();
        foreach (var generator in generatorsArray)
        {
            var leftForStreak = generator.Streak - streaks[generator.Id];
            for (var i = 0; i < leftForStreak; i++)
                variants.Add(generator);
        }
        return variants.Count > 0
            ? variants[random.Next(variants.Count)]
            : alternativeSelector.SelectGenerator(generatorsArray, streaks);
    }
}

В качестве альтернативного селектора в программе используется объект следующего класса:

public class RandomTaskGeneratorSelector : ITaskGeneratorSelector
{
    private readonly Random random;

    public Result<TaskGenerator, Exception> SelectGenerator(
        IEnumerable<TaskGenerator> generators,
        Dictionary<Guid, int> streaks)
    {
        var generatorsArray = generators as TaskGenerator[] ?? generators.ToArray();
        return generatorsArray.Length == 0 
            ? new ArgumentOutOfRangeException(nameof(generators), 
                                              $"{nameof(generators)} must be not empty") 
            : generatorsArray[random.Next(generatorsArray.Length)].Ok();
    }
}

Он выбирает генератор абсолютно случайным образом 

Пример

Пусть есть 2 генератора:

  1. Генератор №1, необходимое кол-во решений: 3
  2. Генератор №2, необходимое кол-во решений: 7

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

Скриншоты работы программы

Телеграм Бот

Приветственное сообщение

Приветственное сообщение

Выбор уровня

Выбор уровня
на /level0 можно кликнуть и перейти к заданиям

Задание

Задание
a, y, m, константы и тело цикла рандомизированы 

Сообщение о полном прохождении уровня

Полное прохождение
Затем следует новая задача

Получение подсказки

Подсказка
Кнопка пропадает, если подсказки кончились

Жалоба на задачу

Жалоба
После этого выдается новая задача

Канал с жалобами пользователей

Сообщение с жалобой

Жалоба в канале

Редактор задач

Авторизация

Авторизация

Главное меню

Редактор

Литература

  1. Языки, грамматики, распознаватели - А. П. Замятин, А. М. Шур, 2007
  2. Telegram Bots Book @ https://telegrambots.github.io/book/
  3. ASP.NET Core in Action - Andrew Lock, 2018
  4. Руководство по языку C# @ https://docs.microsoft.com/ru-ru/dotnet/csharp/
  5. ASP.NET MVC 3 Framework с примерами на C# для профессионалов Адам Фримен , Стивен Сандерсон, 2011
Clone this wiki locally