Skip to content

LOG: Anton

Антон Войцишевский edited this page Jun 10, 2019 · 3 revisions

Проблема

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

Цель

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

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

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

Архитектура

Core

Здесь находится бизнес-логика викторины, построенная согласно DDD:

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: состояниями пользователей и их аутентификационными данными для Quiz API.

Editor

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

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

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

Решение

Структура

Для взаимодействия с внешним API создана сущность генератора:

public abstract class TaskGenerator : Entity
{
    public abstract Task GetTask(Random randomSeed);
}

где Task = struct { string Text; string Question; string Answer; string[] Hints; } Для обеспечения "чистоты" генератора, генератор случайных чисел вынесен в аргумент функции, что позволяет адекватно тестировать генераторы в целом. Так же это является точкой расширения, с помощью наследования и полиморфизма возможно создать генератор, который будет работать на любом принципе, не только на шаблонах.

Генератор, работающий на шаблонах, вынесен в отдельный класс:

public class TemplateTaskGenerator : TaskGenerator
{
   public string[] PossibleAnswers { get; }

   public string Text { get; }

   public string Question { get; }
   
   public string[] Hints { get; }
   
   public string Answer { get; }
}

Шаблонный генератор одновременно является шаблоном для себя же: каждое поле в этом классе содержит текст вместе с некоторой подстановкой, которое в последствии преобразуется в объект Task с помощью движка шаблонизации.

Шаблонизация

Шаблонизация реализована с помощью библиотеки Scriban.

Введем необходимые понятия: Подстановка - некоторая строка вида "{{ smth }}. Шаблон - некоторая строка, которая может содержать в себе подстановки - "usual text {{ substitution }} usual text" Рендеринг шаблона - процесс замены подстановок сгенерированными значениями.

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

Template

"{{ var1 = 5 }} Some number is {{ var1 }} and this is incremented number {{ var1 + 1 }}"

Rendered

" Some number is 5 and this is incremented number 6"

Конкатенация

Методы библиотеки Scriban, осуществляющие парсинг и рендеринг шаблона, имеют следующую сигнатуру:

Template Parse(string rawText);

string Render(Template template, IScriptObject so);

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

public sealed class Storage : IEquatable<Storage>
    {
        private Storage(ICollection<string> items)
        {
            if (items == null)
                throw new ArgumentException("Can not split null");
            Key = Guid.NewGuid().ToString();
            Count = items.Count - 1;
            Concatenated = items.Count == 0 ? "" : Join(Key, items);
        }

        private Storage(string value, string key, int count) => (Concatenated, Key, Count) = (value, key, count);

        private int Count { get; }

        private string Concatenated { get; }

        private string Key { get; }

        [Pure]
        public static Storage Concat(params string[] items) => new Storage(items);

        [Pure]
        public string[] Split() => Concatenated?.Length == 0
                                       ? new string[0]
                                       : Concatenated?.Split(Key)?.ToArray() ?? new string[0];

Логика работы такова: строки конкатенируются, при этом в качестве разделителя используется Guid. В силу того, что его значение уникально, строку после преобразования можно разбить снова по его значению.

Итого

Метод TemplateTaskGenerator.GetTask выглядит так:

public override Task GetTask(Random randomSeed)
{
            var so = CreateScriptObject(randomSeed);

            var simpleFieldsStorage = Concat(Text, Answer, Question);
            var hintsStorage = Concat(Hints ?? new string[0]);
            var answersStorage = Concat(PossibleAnswers ?? new string[0]);

            var fields = new[] { simpleFieldsStorage, hintsStorage, answersStorage };

            var ((code, answer, question), hints, answers)
                = fields.MapMany(vs => Concat(vs).Map(s => Template.Parse(s).Render(so)).Split())
                        .Select(r => r.Split().ToArray())
                        .ToArray();
            return new Task(code, hints, answer, Id, answers, question);
}

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

Дополнительные возможности

В язык шаблонизации добавлено некоторое количество встроенных функций и переменных, которые позволяют делать более интересные и необычные задачи. Например any_of:

Шаблон

var template = 
@"{{ var = any_of ["i", "j"]                                 //| здесь в подстановке определяем глобальные переменные
    c = any_of ["c", "const", "veryImportantVariable"]       //| так как в подстановке только операции присваивания
    inc = random 2 5                                         //| то подстановка возвращает ничего и результатом рендеринга
    op = c + " += " + var + ";"                              //| будет пустая строка
}} for (var {{var}} = 0; {{var}} < n; {{var}} += {{inc}})    //| здесь используем ранее определенные переменные в шаблоне
{                                                            //|
  {{op}}                                                     //|
}"                                                           //|

Результат:

for (var i = 0; i < n; i += 4)
{
  veryImportantVariable += i;
}

Хранение в базе данных

В проекте используется паттерн "Неизменяемая структура данных", в том числе и для генераторов. В качестве БД выбрана MongoDB. К сожалению, сериализатор по умолчанию не поддерживает работу с неизменяемыми классами, для чего в документации было найдено следующее решение:

public static class MongoHelpers
{
        public static void AutoRegisterClassMap<TClass>(Expression<Func<TClass, TClass>> creatorLambda) =>
            AutoRegisterClassMap<TClass>(cm => cm.MapCreator(creatorLambda));

        public static void AutoRegisterClassMap<T>(Action<BsonClassMap<T>> additionalAction = null)
        {
            BsonClassMap.RegisterClassMap<T>(cm =>
            {
                var propertyInfos = typeof(T)
                    .GetProperties(BindingFlags.DeclaredOnly |
                                   BindingFlags.Public |
                                   BindingFlags.Instance)
                    .Cast<MemberInfo>()
                    .ToList();
                foreach (var propertyInfo in propertyInfos)
                    cm.MapMember(propertyInfo);
                additionalAction?.Invoke(cm);
            });
        }
}

Для каждого класса конфигурируется фабричный метод, которым де-факто является конструктор этого класса:

AutoRegisterClassMap<TemplateTaskGenerator>(c => new TemplateTaskGenerator(c.Id, c.PossibleAnswers,
                c.Text, c.Hints,
                c.Answer, c.Streak, c.Question));

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

Телеграм Бот

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

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

Выбор уровня

Выбор уровня
на /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