-
Notifications
You must be signed in to change notification settings - Fork 1
LOG: CGOptimum
- В обучении есть темы, которые нужно закреплять практикой. Каждому студенту требуется свое количество практики. Одна из таких тем - это оценка сложности алгоритмов.
- Студенты не всегда готовы выделить время за компьютером для дополнительной практики, но готовы тренироваться по несколько минут в день на телефоне, в моменты, когда есть свободное время без доступа к полноценному компьютеру. Сейчас они тратят это время на соц-сети, а могут тратить на обучение.
- В рамках командного проекта сделать бота для телеграмма, который позволит тренироваться в оценке сложности алгоритмов в свободное время прямо на своем телефоне. Для этого разработать механику, плавного увеличения сложности задач, с учетом успеваемости пользователя. Задания должны быть каждый раз уникальными и генерироваться на лету, чтобы исключить возможность запоминания уже увиденных вариантов.
- Обобщить бота так, чтобы его можно было использовать для других тем, отличных от оценки сложности алгоритмов.
- Обобщить код так, чтобы незначительными доработками можно было сделать его доступным через другие платформы для чат-ботов.
Решение построено из микросервисов, взаимодействующих между собой по протоколу HTTP.
Бизнес-логика викторины, построенная согласно принципам DDD.
Внешнее REST API для работы с задачами и уровнями. Построено на фреймворке ASP.NET Core.
Внешнее REST API для работы с викториной и пользователями. Построено на фреймворке ASP.NET Core.
Интерфейс для работы с MongoDB: заданиями, пользователями и другой статистикой.
Построенный на ASP.NET Core webhook-bot для Telegram.
Интерфейс для работы с MongoDB: состояниями пользователей и их данными для аутентификации в Task API.
Веб-сервис для редактирования и дизайна уровней викторины. Фронтенд написан на React JS, бекэнд - на ASP.NET Core.
- Создать удобное API для викторины
- Реализовать логику викторины:
- Поиск задания и прогресса пользователя в базе данных
- Выбор задания из предложенных вариантов
- Обновление базы данных с учетом новых изменений
- Защитить логику от действий, недоступных в данный момент конкретному пользователю
Для того, чтобы не отдавать пользователю всю информацию о теме, уровне или задаче (например секретные данные, такие как правильный ответ на вопрос) и некоторые данные отдавать порционно (например подсказки) были созданы отдельные сущности, названные 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; }
}
Интерфейс сервиса викторины использует эти классы внутри возвращаемого значения, которое представляет из себя объект класса-обертки:
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);
}
Получение списка тем
Получение списка всех уровней, которые есть в теме Может вернуть ArgumentException, если передан несуществующий ID темы
Получение списка уровней, доступных пользователю, которые есть в теме Может вернуть ArgumentException, если передан несуществующий ID темы
Получение прогресса пользователя в уровне Может вернуть ArgumentException, если передан несуществующий ID темы или уровня, AccessDeniedException, если у пользователя еще нет доступа к этому уровню
Получение задачи из темы и уровня Может вернуть ArgumentException, если передан несуществующий ID темы или уровня, AccessDeniedException, если у пользователя еще нет доступа к этому уровню
Получение следующей задачи из текущих темы и уровня пользователя Может вернуть AccessDeniedException, если у пользователь еще не начал ни одного уровня
Проверка правильности ответа на текущую задачу Может вернуть AccessDeniedException, если у пользователь еще не начал задачу
Получение подсказки для текущей задачи Может вернуть AccessDeniedException, если у пользователь еще не начал задачу, OutOfHintsException, если подсказки кончились
Для каждого метода написана XML документация, для удобного использования API другими членами команды
Для реализации логики был создан следующий класс:
public class QuizService : IQuizService
{
private readonly ITaskGeneratorSelector generatorSelector;
private readonly Random random;
private readonly ITaskRepository taskRepository;
private readonly IUserRepository userRepository;
}
Объекты для работы с репозиторием задач и пользователей соответственно
Объект для рандомизированного выбора генератора и создания задачи
Объект для выбора генератора из списка, он нем подробно написано ниже
- Проверка правильности переданных аргументов (ID)
- Обновление прогресса пользователя в случае изменений в базе данных заданий
- Проверка доступности данного метода для пользователя
- Выполнение логики метода
- Обновление прогресса пользователя
- Возвращение результата
Для выбора генератора из списка, было решено использовать шаблон “Стратегия”. Интерфейс 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, необходимое кол-во решений: 3
- Генератор №2, необходимое кол-во решений: 7
Распишем 3 шага развития игры:
Как только пользователь полностью решает все генераторы, стратегия меняется на полностью случайный выбор генератора, чтобы была возможность бесконечной игры.
на /level0 можно кликнуть и перейти к заданиям
a, y, m, константы и тело цикла рандомизированы
Затем следует новая задача
Кнопка пропадает, если подсказки кончились
После этого выдается новая задача
- Языки, грамматики, распознаватели - А. П. Замятин, А. М. Шур, 2007
- Telegram Bots Book @ https://telegrambots.github.io/book/
- ASP.NET Core in Action - Andrew Lock, 2018
- Руководство по языку C# @ https://docs.microsoft.com/ru-ru/dotnet/csharp/
- ASP.NET MVC 3 Framework с примерами на C# для профессионалов Адам Фримен , Стивен Сандерсон, 2011